views:

78

answers:

2

I'm considering to use new to redefine members in subclasses to make more specific return type available. The question is: is this a good idea, or is it asking for troubles?

The problem to be solves is when there are several "mirrored" class hierarchies, where types from every hierarical layer are referencing to the same level of another hierarchy. It's hard to explain, so here is an example:

class A1 {}
class A2 : A1 {}
class A3 : A2 {}

abstract class B1
{
  protected A1 myA;

  A1 MyA { get { return myA; } }
}

abstract class B2 : B1
{
  new A2 MyA { get { return (A2)myA; } }
}

class B3 : B2
{
  new A3 MyA 
  { 
    get { return (A3)myA; } 
    set { myA = value; }
  }
}

There is code which only handles the abstract "level" 1 (A1 / B1 etc), just as well for every other level. It would be nice when each "level" would see the types as specific as possible. Currently, I defined the types on the top most level and downcast it all the time in the calling code. Sometimes I add another member for some "level" using another name, this results in many more redundant members. I want to make it cleaner.

(Generics are not an option here, because B2 wouldn't be useful without knowing B3 (it would need additional interfaces there). And if there are many types (eg. also C1/2/3, D1/2/3 ...) there would be too many generic arguments. Technically it would work, but it just gets very complicated.)

Additionally I will serialize some of these classes (DataContractSerializer) and some will be mapped to a database using NHibernate.

Did anyone already do something like this? Is it recommendable? Are there any other approaches to solve the same kind of problem?

Edit: I changed the code a bit because of the discussion about the casts and the implementation of the properties. This is actually not the question here. I just tried to write as little code as possible to make a short example.

+2  A: 

I think you're doing it backwards... downcasts will fail, you should design it with upcasts. Implementation of IEnumerable is the most common example of this pattern.

The most derived type needs to be responsible for creating the most derived result, then virtual methods in the base type can be overridden to return the most derived result via an upcast (which might be implicit).

Ben Voigt
Could you give a quick example?
Grzenio
I'm doing this in many cases, but it results in much too many redundant members which all need a different name. By the way, downcasts will not fail, the most derived class ensures that the most derived types are available.
Stefan Steinegger
It seems to be a defect in C# that explicit override syntax is allowed for interfaces but not base classes. If that were fixed, you wouldn't need so distinct names. Some other .NET languages don't have this restriction, it's specifically a C# issue.
Ben Voigt
When I say "downcasts will fail", I'm thinking that the method creates an object to return. If it's returning a field initialized by the most derived class then the cast will succeed (albeit with a performance cost), but that just moves the complication to the constructors, which have to get the base field initialized with a derived object.
Ben Voigt
On the other hand, what you propose could be useful in protected members to implement the hidden ones. However. It's just the question about using `new` to provide more specific return types.
Stefan Steinegger
@Ben: yes, explicit implementation of abstract members would make it much easier.
Stefan Steinegger
+2  A: 

I would not do it.

It is just to save typing anyway and it will lower your performance since B3.MyA will essentially do return (A3)(A2)this.MyA;. Too many unnecessary casts while you can do b3.MyA as A3 directly only if neccessary.

Also if you forget to initalize MyA in B3 to an object of type A3, your cast will fail, since it will be an object of type A1 or A2 or whatever. You can easily shoot yourself in the leg.

Edit: The point of what I have written is: it is NOT a typesafe way to do it and it never will be. You can minimise the risk of doing that (such as your edited code shows) but the possibility is still there. IMHO the benefits of such a construct do not justify the dangers.

Edit2: Note to your Technically it would work, but it just gets very complicated. in generic approach.

Not so complicated IMHO. For comparison:

public abstract class B1<TA, TC> where TA: A1 where TC: A1
{
    public TA MyA { get; protected set; }
    public TC MyC { get; protected set; }
}

public abstract class B2<TA, TC> : B1<TA, TC> where TA : A2 where TC : A2
{
}

public class B3 : B2<A3, A3>
{
}

Less code, better safety and remember you can always use the using statement if you dislike too much typing:

using ClassicB2 = MemberHiding.B2<MemberHiding.A3, MemberHiding.A3>;
ClassicB2 b3 = new B3();
Jaroslav Jandek
Good point with the casts, I fixed the example code to avoid this.
Stefan Steinegger
Better, but you could still mess it up by doing `this.myA = new A1();` in one of your classes and get an `InvalidCastException`. But that's just nitpicking :)
Jaroslav Jandek
@Stefan: Another problem is that you can't get `A` without the unnecessary casting using the accessor (you could access the field but you would bypass possible functionality implemented in the accessor). Question is: how many times in your class you really need to cast `MyA` to the `A3` type? Only if you need to access new members not implemented in the `A1`. Mostly you should use virtual overridden methods that do not need casting to behave differently for objects of different types.
Jaroslav Jandek
Typesafety: now I'm casting in the calling code. By casting within the class, I could more easily make sure that it never fails. I could even avoid the cast somehow (eg. by protected virtual/overriden members).
Stefan Steinegger
@Stefan: True, but consider that you are casting in the calling code **only** when it is **necessary** (if you don't, you should) => if the cast fails, you can't proceed anyway.In your code, you will cast **always** => even if **unnecessary**. Also you will always get an **exception** instead of possibility of detection and exception-less handling of the situation in calling code. It is simply *less typing* (sometimes) vs. *correctness and performance* problem.
Jaroslav Jandek
@Jaroslav: Sure. But again: I could avoid these casts with some effort (not when they are in the calling code). The question is more about the use of `new` to "override" return types, however it would be implemented.
Stefan Steinegger
@Stefan: IMHO in the case of `new`, the implementation is what matters. If you implement it wrong (like the code in your question - I know it was just an example) you can do more damage than good. That is why I am discussing the implementation. As long as you bear in mind that your `new` properties might not get called every time (`listOfB2[0].MyA` will call B2's `MyA` even if the instance there is `B3`). That is why it is so dangerous with inheritance. I would use such a construct **only** for **private** use within a class where I am always working with the correct type (or not at all).
Jaroslav Jandek
@Stefan: so to answer your question (*is this a good idea, or is it asking for troubles*):No and yes. Also consider that even if **you** know how the class behaves, the other programmers on the project may not know that or forget. I bet you can imagine the consequences. But if you **only** use the **accessor** *internally* or *externaly* with the **proper type**, it should always return the type you expect (with the possibility of occassional exception). Avoiding the cast: only if you make a private **readonly** field (same reference as `myA`), otherwise it would be even more mess!
Jaroslav Jandek
@Jaroslav: ok, thanks for your advise. But the generic approach is definitly not appropriate in this case. Again, there would not only be two arguments, but four, five or even more. Every class (eg. 5 levels of hierarchy times 5 root types) needs an interface without generic arguments, because the code operating with it needs to handle references to it but doesn't know the concrete subtypes (which could be defined in another assembly). Believe me, it gets very complicated.
Stefan Steinegger
@Stefan: You're welcome. It is a shame, the generic version would really work great in most cases :(. I guess I have pointed out everything I should. Good luck with your project.
Jaroslav Jandek