Generics и casting - не могут передавать унаследованный класс в базовый класс
Я знаю, что это старо, но я все еще не очень хорошо разбираюсь в этих проблемах. Может ли кто-нибудь сказать мне, почему следующее не работает (выбрасывает исключение runtime
о кастинге)?
public abstract class EntityBase { }
public class MyEntity : EntityBase { }
public abstract class RepositoryBase<T> where T : EntityBase { }
public class MyEntityRepository : RepositoryBase<MyEntity> { }
И теперь линия литья:
MyEntityRepository myEntityRepo = GetMyEntityRepo(); // whatever
RepositoryBase<EntityBase> baseRepo = (RepositoryBase<EntityBase>)myEntityRepo;
Итак, может ли кто-нибудь объяснить, как это недопустимо? И, я не в настроении объяснить - есть ли строка кода, которую я могу использовать, чтобы на самом деле сделать это?
Ответы
Ответ 1
RepositoryBase<EntityBase>
не является базовым классом MyEntityRepository
. Вы ищете обобщенную дисперсию, которая существует в С# в ограниченной степени, но не будет применяться здесь.
Предположим, что у вашего класса RepositoryBase<T>
был такой метод:
void Add(T entity) { ... }
Теперь рассмотрим:
MyEntityRepository myEntityRepo = GetMyEntityRepo(); // whatever
RepositoryBase<EntityBase> baseRepo = (RepositoryBase<EntityBase>)myEntityRepo;
baseRepo.Add(new OtherEntity(...));
Теперь вы добавили другой тип объекта в MyEntityRepository
... и это не может быть прав.
В принципе, общая дисперсия является безопасной только в определенных ситуациях. В частности, общая ковариация (именно то, что вы здесь описываете) безопасна только тогда, когда вы только когда-либо получаете значения "API"; общая контравариантность (которая работает наоборот) безопасна только тогда, когда вы только когда-либо добавляете значения в API (например, общее сравнение, которое может сравнивать любые две формы по площади, можно рассматривать как сравнение квадратов).
В С# 4 это доступно для общих интерфейсов и общих делегатов, а не для классов - и только для ссылочных типов. См. MSDN для получения дополнительной информации, прочитайте <plug> прочитайте С# в Глубина, 2-е издание, глава 13 </plug> или Эрик Липперт серия блога по этой теме. Кроме того, я рассказал об этом в NDC в июле 2010 года - видео доступно здесь; просто найдите "дисперсию".
Ответ 2
Всякий раз, когда кто-то задает этот вопрос, я пытаюсь взять их пример и перевести его на что-то, используя более известные классы, которые явно незаконны (это то, что Джон Скит сделал в своем ответ, но я делаю это еще дальше, выполняя этот перевод).
Позвольте заменить MyEntityRepository
на MyStringList
следующим образом:
class MyStringList : List<string> { }
Теперь вы, похоже, хотите, чтобы MyEntityRepository
был спрятан до RepositoryBase<EntityBase>
, аргументация заключалась в том, что это должно быть возможным, поскольку MyEntity
происходит от EntityBase
.
Но string
происходит от object
, не так ли? Таким образом, по этой логике мы сможем сделать a MyStringList
до List<object>
.
Посмотрим, что может произойти, если мы допустим это...
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);
}
О-оу. Внезапно мы перечислим List<string>
, и мы наталкиваемся на объект Random
. Это нехорошо.
Надеюсь, это затруднит понимание проблемы.
Ответ 3
Это требует ковариации или контравариантности, поддержка которых ограничена в .Net и не может использоваться в абстрактных классах. Вы можете использовать дисперсию на интерфейсах, так что возможным решением вашей проблемы является создание IRepository, который вы используете вместо абстрактного класса.
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;