2009-07-13 2 views
33

Следующий код:"Интерфейс не реализован" Возвращая производный тип

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; } } 
} 

Выдает следующую ошибку:

error CS0738: 'InheritanceTest.MyData' does not implement interface member 'InheritanceTest.ISomeData.Data'. 'InheritanceTest.MyData.Data' cannot implement 'InheritanceTest.ISomeData.Data' because it does not have the matching return type of 'System.Collections.Generic.IEnumerable'.

Поскольку список <T> реализует IEnumerable <T>, можно было бы подумать что мой класс будет реализовывать интерфейс. Может ли кто-нибудь объяснить, что такое обоснование для этого не компиляции?

Как я могу видеть это, есть два возможных решения:

  1. Изменение интерфейса, чтобы быть более конкретным и требуют IList быть реализованы.
  2. Измените мой класс (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#, если кто-то не может просветить меня.

+0

..Это не является недостатком, просто еще один пример C#, просто делающий то, что вы ему рассказываете. У VB.NET есть много маленьких ситуаций, подобных этому (неуверенный об этом конкретном, хотя), где они компилятор скажет: «Я знаю, что вы действительно хотите», и не будет пытаться выправить его; C# скажет: «Мне все равно, чего вы хотите, все, что я знаю, это то, что вы говорите мне, не летает». – STW

+5

Я до сих пор не согласен и думаю, что это недостаток. Интерфейс говорит, что «вы должны предоставить данные, которые могут быть перечислены», и мой класс, в котором говорится: «Вот данные, которые вы можете перечислить и сделать произвольный доступ к И и т. Д. И т. Д.». Однако компилятор говорит: «Вы не выполняете контракт!». – Daniel

+3

@ Daniel, но если интерфейс указывает только, что вы предоставляете данные, которые могут быть перечислены, то он никогда не позволит вызывающему пользователю воспользоваться всеми другими функциями. Ограничив вас, чтобы выставляли * просто * объект, совместимый с IEnumerable, он заставляет вас осознанно осознавать, что любое другое добавление значений List ** никогда не будет когда-либо когда-либо когда-либо доступным для вызывающего. Это делает вас совестью. Это мешает вам завтра приходить и спрашивать: «MyData.Data предоставляет список , но когда потребительский класс работает с ним, я не могу позвонить .Add()! WTC! C# stoopid!» – STW

ответ

20

К сожалению, тип возвращаемого значения должны совпадать. То, что вы ищете, называется «ковариация возвращаемого типа», а C# не поддерживает это.

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=90909

Эрик Липперт, старший разработчик в команде C# компилятор, упоминает в своем блоге, что они не планируют поддерживать тип возвращаемого ковариации.

"That kind of variance is called "return type covariance". As I mentioned early on in this series, (a) this series is not about that kind of variance, and (b) we have no plans to implement that kind of variance in C#. "

http://blogs.msdn.com/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx

Это стоит прочитать статьи Эрика на ковариации и контрвариации.

http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx

+0

Почему это не поддерживается? Кажется логичным? –

1

Интерфейсы требуют, чтобы подпись метода совпала с подписью договора точно.

Вот простой пример, который также не будет компилироваться:

interface IFoo 
{ 
    Object GetFoo(); 
} 

class Foo : IFoo 
{ 
    public String GetFoo() { return ""; } 
} 

Теперь о том, что делать об этом, я позволил бы интерфейс диктовать реализацию. Если вы хотите, чтобы контракт был IEnumerable<T>, то это то, что должно быть в классе. Интерфейс является самым важным здесь, поскольку реализация вольна быть такой же гибкой, как и должна быть.

Просто убедитесь, что IEnumerable<T> - лучший выбор здесь. (Все это очень субъективно, поскольку я мало знаю о вашем домене или цели этих типов в вашем приложении. Удачи!)

0

Что делать, если вы изменили свой интерфейс, чтобы расширить 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(); 
    } 
} 
3

Подпись члена не может быть разной.

Вы все еще можете вернуть List<string> в методе get, но подпись должна быть такой же, как и у интерфейса.

Так просто изменить:

public List<string> Data { get { return m_MyData; } } 

в

public IEnumerable<string> Data { get { return m_MyData; } } 

касается другого варианта: изменения интерфейса для возврата List. Этого следует избегать. Плохое encapsulation и считается code smell.

+0

Делая это, вы ограничиваете всех, кто использует класс MyData, напрямую, чтобы взаимодействовать с данными только перечислимыми способами. Это довольно ограничивает. (Если пользователь не будет возвращен в список, но это будет очень плохое решение). – Daniel

+2

Да, это ограничение, и это хорошая вещь (tm). Другие методы позволят изменять данные, если это необходимо. Возврат списка позволит другим классам управлять внутренней структурой класса MyData. Это приводит к зависти и будет плохой реализацией. Если требуется модификация структуры данных, MyData должен это сделать, никто другой. –

0

Вы могли бы реализовать так:

public class MyData : ISomeData 
{ 
    private List<string> m_MyData = new List<string>(); 
    public IEnumerable<string> Data { get { return m_MyData; } } 
} 
21

За то, что вы хотите сделать, вы, вероятно, хотите, чтобы реализовать интерфейс явно с членом класса (не интерфейс), который возвращает список вместо 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.

+0

Yooder, спасибо за ваш ответ.Я бы не назвал это симпатичным (не твоя вина), но функционально, он доставит меня туда, куда я хочу. – Daniel

1

Почему не просто возвращать список из вашего интерфейса ...

public interface ISomeData 
{ 
    List<string> Data { get; } 
} 

Если вы знаете, что ваши потребители собираются как итерация над ним (IEnumerable) и добавить к нему (IList), то вполне логично просто верните список.

0

Хм, это недостаток, я бы сказал, нет.

В любом случае, я буду работать вокруг него, как ответ Дарин, либо, если вы явно хотите Список аксессор, а также, вы можете сделать это следующим образом:

public class MyData : ISomeData 
{ 

    IEnumerable<string> ISomeData.Data 
    { 
     get 
     { 
       return _myData; 
     } 
    } 

    public List<string> Data 
    { 
      get 
      { 
      return (List<string>)((ISomeData)this).Data; 
      } 
    } 

} 
4

Что делать, если вы получили доступ вашего объекта MyData через интерфейс ISomeData? В этом случае IEnumerable может быть базового типа, не назначаемого List.

IEnumerable<string> iss = null; 

List<string> ss = iss; //compiler error 

EDIT:

Я понимаю, что вы имеете в виду от ваших комментариев.

Во всяком случае, что я буду делать в вашем случае будет:

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; } } 
    } 

Преобразование общего интерфейса с соответствующим ограничением предложения я думаю, что самое лучшее и гибкости и удобства чтения.

+0

Это единственный ответ, который даже удаленно решает, почему это не является недостатком C#. – womp

+0

Ну, я считаю, что это хороший контракт. –

+0

Интерфейс определяет только свойство get для этого свойства, поэтому нет контекста, в котором вы могли бы подумать, что присвоение IEnumerable будет разрешено. Я вижу, что вы говорите, но поскольку я не указывал набор, ваша точка недействительна в этом примере. – Daniel

0

я бы выбрал вариант 2:

Точки определить интерфейс в коде, чтобы определить контракт, и поэтому вы и другие люди, которые реализуют свой интерфейс знать, что согласны. Независимо от того, задаете ли вы IEnumerable или List в своем интерфейсе, действительно проблема контракта и относится к руководству по разработке структуры. Вот whole book, чтобы обсудить это.

Лично я бы разоблачил IEnumerable и внедрил MyData в IEnumerable, и вы можете вернуть его обратно в метод List в RandomAccess().

+0

Итак, вы говорите, что вы бы выполнили более ограничительный контракт/интерфейс, тогда вы проигнорировали бы его, перейдя в список. Это плохой дизайн, потому что вы осознаете внутреннюю работу класса. Более того, это касается интерфейса, в котором говорится: «Вы должны хотя бы предоставить данные, которые можно перечислить», и класс, в котором говорится: «Вот данные, которые вы можете перечислить и сделать произвольный доступ к AND et et». Однако компилятор говорит: «Вы не выполняете контракт!» – Daniel

+0

Нет, я НЕ предполагаю, что вы намеренно передали его конкретному списку типов, если интерфейс, который вы реализуете, является IEnumerable. Я просто предлагаю, чтобы у вас было обходное решение, если вы перейдете к опции2. –

0

Если нужно иметь список в интерфейсе (известное количество элементов с произвольным доступом), то вы должны рассмотреть возможность изменения интерфейса к

public interface ISomeData 
{ 
    ICollection<string> Data { get; } 
} 

Это даст вам как растяжимость и тому функции, которые вам нужны из списка.

List<T> не может быть легко подклассифицирован, что означает, что у вас могут возникнуть проблемы с возвратом точного типа из всех классов, которые хотят реализовать ваш интерфейс.

ICollection<T>, с другой стороны, может быть реализован различными способами.

0

Я столкнулся с подобными ситуациями и хочу дать более конкретный пример того, почему это запрещено. Общие части моего приложения связаны с интерфейсом, который содержит свойства, и предоставляет данные через этот интерфейс в другую часть приложения, содержащую конкретные классы, реализующие эти интерфейсы.

Цель, которая, как я думаю, похожа на вашу: классы классов, которые имеют больше функциональности, а интерфейс обеспечивает абсолютный минимум, необходимый для общей части приложения. Для интерфейса очень полезно выявить наименьшее количество функциональных возможностей, поскольку оно максимизирует совместимость/повторное использование. То есть он не требует, чтобы разработчик интерфейса реализовал больше, чем это необходимо.

Однако учтите, был ли у вас сеттер на этом свойстве. Вы создаете экземпляр вашего конкретного класса и передаете его некоторому универсальному помощнику, который принимает ISomeData. В коде этого помощника они работают с ISomeData. Без какого-либо типа T, где T: new() или заводской шаблон, они не могут создавать новые экземпляры для ввода данных, которые соответствуют вашей конкретной реализации. Они просто возвращают список, который реализует IEnumerable:

instanceISomeData.Data = new SomeOtherTypeImplementingIEnumerable();

Если SomeOtherTypeImplementingIEnumerable не наследует List, но вы реализовали .Data как список, то это недопустимое присвоение. Если компилятор разрешил вам это сделать, тогда сценарии, подобные этому, будут повреждены во время выполнения, потому что SomeOtherTypeImplementingIEnumerable не может быть передан в List. Однако в контексте этого помощника, работающего с ISomeData, он все равно не нарушал интерфейс ISomeData, и назначение любого типа, поддерживающего IEnumerable, должно быть действительным. Таким образом, ваша реализация, если бы это разрешила компилятор, могла нарушить превосходный код, работающий с интерфейсом.

Вот почему вы не можете реализовать. Данные как производный тип. Вы более жестко ограничиваете реализацию .Data только за исключением List, а не любого IEnumerable. Таким образом, хотя интерфейс говорит, что «любой IEnumerable разрешен», ваша конкретная реализация заставит ранее существовавший код, поддерживающий ISomeData, внезапно сломаться при работе с вашей реализацией интерфейса.

Конечно, вы не сталкиваетесь с этим сценарием только с помощью геттера, но это усложнит ситуацию, позволяя ему получать сценарии, но не иначе.

Обычно я хожу с решением Джейка или решением Алиото, в зависимости от того, насколько я себя чувствую в настоящий момент.