Generics and casting - cannot cast inherited class

2019-01-01 15:41发布

问题:

I know this is old, yet I am still not very good with understanding those problems. Can anyone tell me why the following does not work (throws a runtime exception about casting)?

public abstract class EntityBase { }
public class MyEntity : EntityBase { }

public abstract class RepositoryBase<T> where T : EntityBase { }
public class MyEntityRepository : RepositoryBase<MyEntity> { }

And now the casting line:

MyEntityRepository myEntityRepo = GetMyEntityRepo(); // whatever
RepositoryBase<EntityBase> baseRepo = (RepositoryBase<EntityBase>)myEntityRepo;

So, can anyone explain how is this invalid? And, I you are not in the mood to explain - is there a line of code I can use to actually do this cast?

回答1:

RepositoryBase<EntityBase> is not a base class of MyEntityRepository. You\'re looking for generic variance which exists in C# to a limited extent, but wouldn\'t apply here.

Suppose your RepositoryBase<T> class had a method like this:

void Add(T entity) { ... }

Now consider:

MyEntityRepository myEntityRepo = GetMyEntityRepo(); // whatever
RepositoryBase<EntityBase> baseRepo = (RepositoryBase<EntityBase>)myEntityRepo; 
baseRepo.Add(new OtherEntity(...));

Now you\'ve added a different kind of entity to a MyEntityRepository... and that can\'t be right.

Basically, generic variance is only safe in certain situations. In particular generic covariance (which is what you\'re describing here) is only safe when you only ever get values \"out\" of the API; generic contravariance (which works the other way round) is only safe when you only ever put values \"into\" the API (e.g. a general comparison which can compare any two shapes by area can be considered as a comparison of squares).

In C# 4 this is available for generic interfaces and generic delegates, not classes - and only with reference types. See MSDN for further information, read <plug>read C# in Depth, 2nd edition, chapter 13</plug> or Eric Lippert\'s blog series on the topic. Also, I gave a one hour talk about this at NDC in July 2010 - the video is available here.



回答2:

Whenever someone asks this question, I try to take their example and translate it to something using more well-known classes that is obviously illegal (this is what Jon Skeet has done in his answer; but I\'m taking it a step further by performing this translation).

Let\'s replace MyEntityRepository with MyStringList, like this:

class MyStringList : List<string> { }

Now, you seem to want MyEntityRepository to be castable to RepositoryBase<EntityBase>, the reasoning being that this ought to be possible since MyEntity derives from EntityBase.

But string derives from object, doesn\'t it? So by this logic we should be able to cast a MyStringList to a List<object>.

Let\'s see what can happen if we allow that...

var strings = new MyStringList();
strings.Add(\"Hello\");
strings.Add(\"Goodbye\");

var objects = (List<object>)strings;
objects.Add(new Random());

foreach (string s in strings)
{
    Console.WriteLine(\"Length of string: {0}\", s.Length);
}

Uh-oh. Suddenly we\'re enumerating over a List<string> and we come upon a Random object. That\'s not good.

Hopefully this makes the issue a bit easier to understand.



回答3:

This requires covariance or contravariance, whose support is limited in .Net, and cannot be used on abstract classes. You can use variance on interfaces though, so a possible solution to your problem is to create an IRepository which you use in place of the abstract class.

    public interface IRepository<out T> where T : EntityBase { //or \"in\" depending on the items.
    }
    public abstract class RepositoryBase<T> : IRepository<T> where T : EntityBase {
    }
    public class MyEntityRepository : RepositoryBase<MyEntity> {
    }

    ...

    IRepository<EntityBase> baseRepo = (IRepository<EntityBase>)myEntityRepo;