"Интерфейс не реализован" при возврате производного типа
Следующий код:
public interface ISomeData
{
IEnumerable<string> Data { get; }
}
public class MyData : ISomeData
{
private List<string> m_MyData = new List<string>();
public List<string> Data { get { return m_MyData; } }
}
Выдает следующую ошибку:
Ошибка CS0738: "InheritanceTest.MyData" не реализует элемент интерфейса "InheritanceTest.ISomeData.Data". "InheritanceTest.MyData.Data" не может реализовать "InheritanceTest.ISomeData.Data", поскольку у него нет соответствующего возвращаемого типа "System.Collections.Generic.IEnumerable".
Поскольку List
Насколько я понимаю, есть два возможных решения:
- Измените интерфейс на более конкретный и требующий реализации IList.
- Измените мой класс (MyData), чтобы он возвращал IEnumerable, и реализуйте оригинальный интерфейс.
Теперь предположим, что у меня также есть следующий код:
public class ConsumerA
{
static void IterateOverCollection(ISomeData data)
{
foreach (string prop in data.MyData)
{
/*do stuff*/
}
}
}
public class ConsumerB
{
static void RandomAccess(MyData data)
{
data.Data[1] = "this line is invalid if MyPropList return an IEnumerable<string>";
}
}
Я мог бы изменить свой интерфейс, чтобы требовать реализации IList (вариант 1), но это ограничивает тех, кто может реализовать интерфейс, и количество классов, которые могут быть переданы в ConsumerA. Или я мог бы изменить реализацию (класс MyData) так, чтобы она возвращала IEnumerable вместо List (вариант 2), но тогда ConsumerB пришлось бы переписать.
Это кажется недостатком C#, если кто-то не может просветить меня.
12 ответов
К сожалению, тип возвращаемого значения должен совпадать. То, что вы ищете, называется "ковариация возвращаемого типа", и C# не поддерживает это.
http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=90909
Эрик Липперт, старший разработчик команды C# Compiler, упоминает в своем блоге, что они не планируют поддерживать ковариацию возвращаемого типа.
"Такая дисперсия называется" ковариацией возвращаемого типа ". Как я уже упоминал ранее в этой серии, (а) эта серия не о такой дисперсии, и (б) у нас нет планов по внедрению такой дисперсии в C#. "
Стоит прочитать статьи Эрика о ковариации и контравариантности.
http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx
Для того, что вы хотите сделать, вы, вероятно, захотите реализовать интерфейс явно с помощью члена класса (не интерфейса), который возвращает список вместо IEnumerable...
public class MyData : ISomeData
{
private List<string> m_MyData = new List<string>();
public List<string> Data
{
get
{
return m_MyData;
}
}
#region ISomeData Members
IEnumerable<string> ISomeData.Data
{
get
{
return Data.AsEnumerable<string>();
}
}
#endregion
}
Редактировать: для пояснения, это позволяет классу MyData возвращать List, когда он обрабатывается как экземпляр MyData; по-прежнему позволяя ему возвращать экземпляр IEnumerable при обработке как экземпляр ISomeData.
Что если вы получили доступ к своему объекту MyData через интерфейс ISomeData? В этом случае IEnumerable может иметь базовый тип, не назначаемый списку.
IEnumerable<string> iss = null;
List<string> ss = iss; //compiler error
РЕДАКТИРОВАТЬ:
Я понимаю, что вы имеете в виду из ваших комментариев.
В любом случае, что бы я сделал в вашем случае:
public interface ISomeData<T> where T: IEnumerable<string>
{
T Data { get; }
}
public class MyData : ISomeData<List<string>>
{
private List<string> m_MyData = new List<string>();
public List<string> Data { get { return m_MyData; } }
}
Преобразование в универсальный интерфейс с соответствующими ограничениями предлагает, на мой взгляд, лучшее из гибкости и удобочитаемости.
Подпись участника не может быть другой.
Вы все еще можете вернуть List<string>
в пределах get
метод, но подпись должна совпадать с интерфейсом.
Так что просто поменяйте:
public List<string> Data { get { return m_MyData; } }
в
public IEnumerable<string> Data { get { return m_MyData; } }
Что касается вашего другого варианта: изменение интерфейса для возврата List
, Этого следует избегать. Это плохая инкапсуляция и считается запахом кода.
Вы можете реализовать так:
public class MyData : ISomeData
{
private List<string> m_MyData = new List<string>();
public IEnumerable<string> Data { get { return m_MyData; } }
}
Я бы выбрал вариант 2:
Смысл определения интерфейса в вашем коде состоит в том, чтобы определить контракт, чтобы вы и другие люди, которые реализуют ваш интерфейс, знали, с чем следует согласиться. Независимо от того, определяете ли вы IEnumerable или List в своем интерфейсе, это действительно вопрос контракта и относится к руководству по проектированию инфраструктуры. Вот целая книга, чтобы обсудить это.
Лично я бы выставил IEnumerable и реализовал MyData в IEnumerable, и вы можете привести его обратно к List в методе RandomAccess().
Интерфейсы требуют, чтобы подпись метода точно совпадала с подписью контракта.
Вот более простой пример, который также не будет компилироваться:
interface IFoo
{
Object GetFoo();
}
class Foo : IFoo
{
public String GetFoo() { return ""; }
}
Что касается того, что с этим делать, я бы позволил интерфейсу диктовать реализацию. Если вы хотите, чтобы контракт был IEnumerable<T>
тогда это то, что должно быть в классе. Интерфейс является наиболее важной вещью здесь, поскольку реализация может быть настолько гибкой, насколько это необходимо.
Просто будьте уверены, что IEnumerable<T>
это лучший выбор здесь. (Это все очень субъективно, так как я мало знаю о вашем домене или назначении этих типов в вашем приложении. Удачи!)
Почему бы просто не вернуть список из вашего интерфейса...
public interface ISomeData
{
List<string> Data { get; }
}
Если вы знаете, что ваши потребители собираются итерировать по нему (IEnumerable) и добавлять к нему (IList), тогда кажется логичным просто вернуть List.
Хм, это недостаток, я бы сказал нет.
В любом случае, я бы обошел все вокруг, как ответ Дарина, или, если вы явно хотите использовать средство доступа к списку, вы можете сделать это так:
public class MyData : ISomeData
{
IEnumerable<string> ISomeData.Data
{
get
{
return _myData;
}
}
public List<string> Data
{
get
{
return (List<string>)((ISomeData)this).Data;
}
}
}
Что если вы изменили свой интерфейс, чтобы расширить IEnumerable, чтобы вы могли перечислять объект и редактировать данные через свойство класса.
public interface ISomeData : IEnumerable<string>
{
IEnumerable<string> Data { get; }
}
public class MyData : ISomeData
{
private List<string> m_MyData = new List<string>();
public List<string> Data { get { return m_MyData; }
public IEnumerator<string> GetEnumerator()
{
return Data;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Я сталкивался с подобными ситуациями и хочу привести более конкретный пример того, почему это не разрешено. Общие части моего приложения имеют дело с интерфейсом, который содержит свойства, и предоставляет данные через этот интерфейс к другой части приложения, которая содержит конкретные классы, реализующие эти интерфейсы.
Я думаю, что цель похожа на вашу: конкретные классы должны обладать большей функциональностью, а интерфейс обеспечивает абсолютный минимум, необходимый для общей части приложения. Рекомендуется, чтобы интерфейс предоставлял наименьшее количество необходимой функциональности, поскольку он максимизирует совместимость / повторное использование. Т.е. это не требует реализации интерфейса для реализации больше, чем необходимо.
Однако подумайте, есть ли у вас установщик этого свойства. Вы создаете экземпляр вашего конкретного класса и передаете его некоторому универсальному помощнику, который принимает ISomeData. В коде этого помощника они работают с ISomeData. Без какого-либо общего типа T, где T:new() или фабричного шаблона, они не могут создавать новые экземпляры для ввода в данные, соответствующие вашей конкретной реализации. Они просто возвращают список, который реализует IEnumerable:
instanceISomeData.Data = new SomeOtherTypeImplementingIEnumerable ();
Если SomeOtherTypeImplementingIEnumerable не наследует список, но вы внедрили.Data как список, то это недопустимое назначение. Если компилятор позволил вам сделать это, то сценарии, подобные этому, вылетали бы во время выполнения, потому что SomeOtherTypeImplementingIEnumerable не может быть приведен к List. Однако в контексте этого помощника, работающего с ISomeData, он в любом случае не нарушил интерфейс ISomeData, и назначение любого типа, поддерживающего IEnumerable в.Data, должно быть допустимым. Таким образом, ваша реализация, если это позволяет компилятор, может нарушить совершенно хороший код, работающий с интерфейсом.
Вот почему вы не можете реализовать.Data как производный тип. Вы более жестко ограничиваете реализацию.Data только за исключением List, вместо любого IEnumerable. Таким образом, в то время как интерфейс говорит, что "любой IEnumerable разрешен", ваша конкретная реализация может привести к внезапному разрыву существующего кода, поддерживающего ISomeData, при работе с вашей реализацией интерфейса.
Конечно, вы на самом деле не сталкиваетесь с этим сценарием только с геттером, но это усложнит вещи, чтобы разрешить его в сценариях получения, но не иначе.
Я обычно выбираю решение Джейка или Алиото, в зависимости от того, насколько я разборчива в данный момент.
Если вам нужен список в вашем интерфейсе (известное количество элементов с произвольным доступом), вам следует рассмотреть возможность изменения интерфейса на
public interface ISomeData
{
ICollection<string> Data { get; }
}
Это даст вам как расширяемость, так и функции, которые вам нужны из списка.
List<T>
не может быть легко разделено на подклассы, что означает, что у вас могут возникнуть проблемы с возвратом этого точного типа из всех классов, которые хотят реализовать ваш интерфейс.
ICollection<T>
с другой стороны, могут быть реализованы различными способами.