Унаследованное унифицированное обобщение типа
Для сценария, такого как:
public interface IAnimal
{
}
public interface IGiraffe : IAnimal
{
}
public interface IQuestionableCollection : IEnumerable<IAnimal>
{
void SomeAction();
}
public interface IQuestionableCollection<out T> : IQuestionableCollection, IEnumerable<T>
where T : IAnimal
{
}
public class QuestionableCollection<T> : IQuestionableCollection<T>
where T:IAnimal
{
// Implementation...
}
Компилятор будет генерировать ошибку:
'IQuestionableCollection<T>' cannot implement both 'System.Collections.Generic.IEnumerable<IAnimal>' and 'System.Collections.Generic.IEnumerable<T>' because they may unify for some type parameter substitutions
И это имеет смысл, есть действительно двусмысленность между двумя интерфейсами, которые С# не может решить, если только он не использует ограничение типа, которое не соответствует спецификации языка, поскольку @ericlippert объясняет .
Мой вопрос: как мне реализовать что-то с этим же эффектом?
Похоже, я должен быть в состоянии выразить, что коллекция перечислима для базового интерфейса. (Я хотел бы предоставить набор методов, которые можно было бы использовать, не зная конкретного типа, а также сделать некоторые API/код отражения более чистыми, поэтому я хотел бы сохранить базовую коллекцию как универсальную, если вообще возможно, в противном случае не было бы необходимости в двух интерфейсах.)
Единственная реализация, которую я могу придумать, заключается в следующем:
public interface IQuestionableCollectionBase
{
void SomeAction();
}
public interface IQuestionableCollection : IQuestionableCollectionBase, IEnumerable<IAnimal>
{
}
public interface IQuestionableCollection<out T> : IQuestionableCollectionBase, IEnumerable<T>
where T : IAnimal
{
}
public class QuestionableCollectionBase<T> : IQuestionableCollection
where T : IAnimal
{
protected List<T> _items = new List<T>();
public void SomeAction() { }
IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); }
IEnumerator<IAnimal> IEnumerable<IAnimal>.GetEnumerator() { return ((IEnumerable<IAnimal>)_items).GetEnumerator(); }
}
public class QuestionableCollection<T> : QuestionableCollectionBase<T>, IQuestionableCollection<T>
where T : IAnimal
{
public IEnumerator<T> GetEnumerator() { return ((IEnumerable<T>)_items).GetEnumerator(); }
}
Обратите внимание, что мне пришлось перемещать любые методы, которые я хотел бы использовать на обоих интерфейсах, для базового метода и иметь два уровня реализации для самого класса - похоже, что я прыгаю через достаточно обручей здесь, что я что-то не хватает...
Как это реализовать?
Ответы
Ответ 1
Простейшим обходным решением является изменение IEnumerables с "is-a" на "has-a", например:
public interface IAnimal { }
public interface IGiraffe : IAnimal { }
public interface IQuestionableCollection
{
IEnumerable<IAnimal> Animals { get; }
void SomeAction();
}
public interface IQuestionableCollection<out T> : IQuestionableCollection
where T : IAnimal
{
new IEnumerable<T> Animals { get; }
}
public class QuestionableCollection<T> : IQuestionableCollection<T>
where T : IAnimal, new()
{
private readonly List<T> list = new List<T>();
public IEnumerable<T> Animals
{
get { return list; }
}
IEnumerable<IAnimal> IQuestionableCollection.Animals
{
get { return (IEnumerable<IAnimal>)list; }
}
public void SomeAction()
{
list.Add(new T());
}
}
class Giraffe : IGiraffe { }
[TestMethod]
public void test()
{
var c = new QuestionableCollection<Giraffe>();
IQuestionableCollection<Giraffe> i = c;
IQuestionableCollection<IGiraffe> i2 = i;
Assert.AreEqual(0, c.Animals.Count());
Assert.AreEqual(0, i.Animals.Count());
c.SomeAction();
i.SomeAction();
Assert.AreEqual(2, c.Animals.Count());
Assert.AreEqual(2, i.Animals.Count());
}
Обратите внимание, что вы можете избежать приведения в QuestionableCollection<T>
, если вы добавите ограничение where T : class
.
Ответ 2
Изменение IQuestionableCollection в неэквивалентном IEnumerable сортирует проблемы компилятора.
public interface IQuestionableCollection : IEnumerable {...}
Я видел, как MS использует этот шаблон в своих коллекциях, с неосновными версиями, использующими IEnumerable
, и общие с помощью IEnumerable<T>
.
В качестве альтернативы, если остальные IEnumerable<IAnimal>
также останавливают ошибки компилятора, хотя это означает, что вы переводите IAnimals вместо T при перечислении.
Ответ 3
Вы можете попробовать следующее:
public interface IAnimal
{
}
public interface IGiraffe : IAnimal
{
}
public interface IQuestionableCollection<T> : IEnumerable<T> where T : IAnimal
{
void SomeAction();
}
public interface IQuestionableCollection : IQuestionableCollection<IAnimal>
{
}
public class QuestionableCollection<T> : IQuestionableCollection<T>, IEnumerable<T>
where T : IAnimal
{
public void SomeAction() { }
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
Ответ 4
Учитывая ограничения языка, вы не сможете обойти проблему с IQuestionableCollection и IQuestionableCollection как с внедрением универсального интерфейса.
В сущности, вы указываете, что IQuestionableCollection реализует два возможных списка: список и другой список. Здесь нет иерархических отношений, поэтому невозможно решить.
При этом вам нужно будет заменить IEnumerable на IEnumerable на IQuestionableCollection, чтобы обеспечить цепочку наследования компилятору. На самом деле нет даже смысла объявлять базовый IQuestionableCollection, если вы не планируете внедрять ванильный QuestionableCollection
В моем обновленном коде я взял его для потребителя QuestionableCollection, чтобы проиллюстрировать перечисление QuestionableCollection(), сохраняя типизацию IGiraffe.
public interface IAnimal {}
public interface IGiraffe : IAnimal { }
public interface IQuestionableCollection : IEnumerable
{
void SomeAction();
}
public interface IQuestionableCollection<out T> : IQuestionableCollection, IEnumerable<T>
where T : IAnimal
{ }
public class QuestionableCollection<T> : IQuestionableCollection<T>
where T : IAnimal
{
private List<T> list = new List<T>();
public IEnumerator<T> GetEnumerator()
{
return list.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void SomeAction()
{
throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
var questionable = new QuestionableCollection<IGiraffe>();
foreach (IGiraffe giraffe in questionable)
{
}
}
}
Для нестандартной реализации
1 Вы всегда можете безопасно повышать
var questionable = new QuestionableCollection();
IEnumerabl<IAnimal> animals = questionable.OfType<IAnimal>();
-или -
2 Вы можете использовать класс NonGeneric QuestionableCollection для IEnumerable
public class QuestionableCollection : IQuestionableCollection, IEnumerable<IAnimal>
{
public IEnumerator<IAnimal> GetEnumerator()
{
var l = new List<Giraffe>();
l.Add(new Giraffe());
l.Add(new Giraffe());
return l.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void SomeAction()
{
throw new NotImplementedException();
}
}
который вы затем можете перечислить без операции трансляции.
var questionable = new QuestionableCollection();
foreach (IAnimal giraffe in questionable)
{
var i = giraffe;
}