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 маркер. Он возвращает то же самое, что и базовый класс, и просто добавляет дополнительные гарантии к контракту функции.

Другие вопросы по тегам