C# ковариантные типы возвращаемых данных, использующие дженерики
Является ли приведенный ниже код единственным способом реализации ковариантных типов возвращаемых данных?
public abstract class BaseApplication<T> {
public T Employee{ get; set; }
}
public class Application : BaseApplication<ExistingEmployee> {}
public class NewApplication : BaseApplication<NewEmployee> {}
Я хочу иметь возможность создать приложение или NewApplication и заставить его возвращать соответствующий тип Employee из свойства Employee.
var app = new Application();
var employee = app.Employee; // this should be of type ExistingEmployee
Я считаю, что этот код работает нормально, но он становится очень неприятным, когда у меня есть несколько свойств, которые требуют одинакового поведения.
Есть ли другие способы реализовать это поведение? Дженерики или нет?
7 ответов
Прежде всего, ответ на ваш вопрос - нет, C# не поддерживает какую-либо форму ковариации возвращаемого типа для виртуальных переопределений.
Ряд ответчиков и комментаторов сказали, что "в этом вопросе нет ковариации". Это неверно; оригинальный постер был совершенно прав, чтобы поставить вопрос так, как они.
Напомним, что ковариантное отображение - это отображение, которое сохраняет существование и направление некоторого другого отношения. Например, отображение из типа T
к типу IEnumerable<T>
является ковариантным, поскольку сохраняет отношение совместимости присваивания. Если Tiger совместим по назначению с Animal, трансформация под картой также сохраняется: IEnumerable<Tiger>
совместимо ли назначение с IEnumerable<Animal>
,
Ковариантное отображение здесь немного сложнее увидеть, но оно все еще там. По сути, вопрос заключается в следующем: это должно быть законно?
class B
{
public virtual Animal M() {...}
}
class D : B
{
public override Tiger M() {...}
}
Тигр совместим по назначению с Animal. Теперь сделайте отображение типа T на метод public T M(). Сохраняет ли это отображение совместимость? То есть, если Tiger совместим с Animal для целей присвоения, то public Tiger M()
совместим с public Animal M()
для целей виртуального переопределения?
Ответ на C# - "нет". C# не поддерживает этот вид ковариации.
Теперь, когда мы установили, что вопрос был задан с использованием правильного жаргона алгебры типов, еще несколько мыслей по фактическому вопросу. Первая очевидная проблема заключается в том, что свойство даже не было объявлено как виртуальное, поэтому вопросы виртуальной совместимости спорны. Очевидная вторая проблема заключается в том, что "get; set;" свойство не может быть ковариантным, даже если C# действительно поддерживает ковариацию возвращаемого типа, потому что тип свойства с установщиком - это не просто его тип возвращаемого значения, это также и его тип формального параметра. Вам нужна противоположность формальных типов параметров для обеспечения безопасности типов. Если бы мы позволили возвращать ковариацию типа для свойств с установщиками, то у вас было бы:
class B
{
public virtual Animal Animal{ get; set;}
}
class D : B
{
public override Tiger Animal { ... }
}
B b = new D();
b.Animal = new Giraffe();
и эй, мы только что передали жирафа сеттеру, который ожидает тигра. Если бы мы поддерживали эту функцию, нам пришлось бы ограничить ее возвращаемыми типами (как мы делаем с ковариацией совместимости присваивания на универсальных интерфейсах).
Третья проблема заключается в том, что CLR не поддерживает этот вид дисперсии; если бы мы хотели поддержать его на языке (как я полагаю, управляемый C++), то нам пришлось бы предпринять некоторые достаточно героические меры, чтобы обойти ограничения на соответствие сигнатур в CLR.
Вы можете выполнить эти героические меры самостоятельно, тщательно определив "новые" методы, которые имеют соответствующие типы возвращаемых данных, которые скрывают их типы базовых классов:
abstract class B
{
protected abstract Animal ProtectedM();
public Animal Animal { get { return this.ProtectedM(); } }
}
class D : B
{
protected override Animal ProtectedM() { return new Tiger(); }
public new Tiger Animal { get { return (Tiger)this.ProtectedM(); } }
}
Теперь, если у вас есть экземпляр D, вы видите свойство типа Tiger. Если вы приведете его к B, вы увидите свойство Animal-typed. В любом случае вы все равно получаете виртуальное поведение через защищенного члена.
Короче говоря, у нас нет планов когда-либо делать эту функцию, извините.
Там может быть несколько проблем с тем, что вы пытаетесь достичь.
Прежде всего, как кто-то уже заметил, в вашем примере нет ковариации. Вы можете найти краткое описание ковариации и дженериков здесь, новые функции в C# 2.0 - дисперсия, ковариация дженериков.
Во-вторых, кажется, что вы пытаетесь решить с помощью дженериков, что следует решать с полиморфизмом. Если оба ExistingEmployee
а также NewEmployee
наследовать от базового класса Employee
Ваша проблема будет решена:
public class Application {
public ExistingEmployee Employee { get; }
}
public class NewApplication {
public NewEmployee Employee { get; }
}
...
Application app = new Application;
var emp = app.Employee; // this will be of type ExistingEmployee!
Обратите внимание, что ниже также верно:
Employee emp = app.Employee; // this will be of type ExistingEmployee even if
// declared as Employee because of polymorphism
Единственное, что будет отличаться между полиморфизмом и обобщениями, это то, что если вы вернете переменную к определенному типу, вам понадобится приведение в более позднем случае:
ExistingEmployee emp = (ExistingEmployee)app.Employee; // would have not been needed
// if working with generics
Надеюсь это поможет.
Вы можете написать код для интерфейса сотрудников, чтобы получить то, что вы хотите, я думаю.
public interface IEmployee
{}
public abstract class BaseApplication<T> where T:IEmployee{
public T IEmployee{ get; set; }
}
public class ExistingEmployee : IEmployee {}
public class NewEmployee : IEmployee {}
public class Application : BaseApplication<ExistingEmployee> {}
public class NewApplication : BaseApplication<NewEmployee> {}
Вы можете получить несколько аккуратную версию этого, используя дженерики.
Ковариантные возвращаемые типы не поддерживаются C#. Так что это не решение, однако, я чувствую, что синтаксически это хорошо читается. Это достигает аналогичного результата.
Я считаю это полезным при создании fluent API's
где базовый класс должен выполнить некоторые действия, но мне нужна возвращенная реализация. Все, чего он действительно добивается, - это скрыть актерский состав.
public class Base
{
public virtual T Foo<T>() where T : Base
{
//... // do stuff
return (T)this;
}
}
public class A : Base
{
public A Bar() { "Bar".Dump(); return this; }
public A Baz() { "Baz".Dump(); return this; }
// optionally override the base...
public override T Foo<T>() { "Foo".Dump(); return base.Foo<T>(); }
}
var x = new A()
.Bar()
.Foo<A>() // cast back to A
.Baz();
Мнения будут меняться, и это не на 100% красиво. Вероятно, он не подходит для API, который будет опубликован, но для внутреннего использования, например, в модульных тестах, я считаю его полезным.
Код, который вы разместили, не будет компилироваться, но я получаю базовое представление о том, что вы хотите сделать. Короче говоря, ответ да, это единственный способ. Если вы хотите, чтобы свойство возвращало разные типы и по-разному печаталось в расширенных классах, то вы должны использовать дженерики так, как вы уже это делаете.
Если вы можете инкапсулировать публичный контракт объекта сотрудника, нового или существующего, в интерфейс, то вам вообще не нужно использовать дженерики. Вместо этого вы можете просто вернуть интерфейс и позволить полиморфизму вступить во владение.
public interface IEmployee
{ }
public class Employee1 : IEmployee
{ }
public class Employee2 : IEmployee
{ }
public abstract class ApplicationBase
{
public abstract IEmployee Employee { get; set; }
}
public class App1 : ApplicationBase
{
public override IEmployee Employee
{
get { return new Employee1(); }
set;
}
}
public class App2 : ApplicationBase
{
public override IEmployee Employee
{
get { return new Employee2(); }
set;
}
}
ДА!! Как это. Котла больше, чем можно было бы надеяться, но он работает. Трюк делается с помощью методов расширения. Это дозирует некоторые неприятные заклинания внутри, но представляет ковариантный интерфейс.
Смотрите также: http://richarddelorenzi.wordpress.com/2011/03/25/return-type-co-variance-in-c/
using System;
namespace return_type_covariance
{
public interface A1{}
public class A2 : A1{}
public class A3 : A1{}
public interface B1
{
A1 theA();
}
public class B2 : B1
{
public A1 theA()
{
return new A2();
}
}
public static class B2_ReturnTypeCovariance
{
public static A2 theA_safe(this B2 b)
{
return b.theA() as A2;
}
}
public class B3 : B1
{
public A1 theA()
{
return new A3();
}
}
public static class B3_ReturnTypeCovariance
{
public static A3 theA_safe(this B3 b)
{
return b.theA() as A3;
}
}
public class C2
{
public void doSomething(A2 a){}
}
class MainClass
{
public static void Main (string[] args)
{
var c2 = new C2();
var b2 = new B2();
var a2=b2.theA_safe();
c2.doSomething(a2);
}
}
}
Одна идея без дженериков, но у нее есть и другие недостатки:
public abstract class BaseApplication {
public Employee Employee{ get; protected set; }
}
public class Application : BaseApplication
{
public new ExistingEmployee Employee{ get{return (ExistingEmployee)base.Employee;} set{base.Employee=value; }}
}
public class NewApplication : BaseApplication
{
public new NewEmployee Employee{ get{return (NewEmployee)base.Employee;} set{base.Employee=value; }}
}
В частности, с помощью этого кода вы можете привести к базовому классу и назначить сотрудника нежелательного типа. Поэтому вам нужно добавить проверки против этого в установщике базового класса. Или удалите сеттер, который я обычно предпочитаю в любом случае. Один из способов сделать это - защитить сеттер.
Другой добавляет виртуальную функцию EmployeeType()
который вы переопределяете в производных классах и возвращаете производный тип. Затем вы проверяете в установщике, если EmployeeType().IsInstanceOf(value)
а еще выкинуть исключение.
ИМО, имитирующий ковариантные типы возврата, является одним из немногих хороших приложений new
маркер. Он возвращает то же самое, что и базовый класс, и просто добавляет дополнительные гарантии к контракту функции.