c # ковариантные типы возвращаемых данных, использующие дженерики - PullRequest
25 голосов
/ 03 декабря 2010

Является ли приведенный ниже код единственным способом реализации ковариантных типов возврата?

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 ]

67 голосов
/ 03 декабря 2010

Прежде всего, ответ на ваш вопрос - нет, C # не поддерживает какую-либо форму ковариации возвращаемого типа для виртуальных переопределений.

Некоторые авторы и комментаторы сказали, что "в этом вопросе нет ковариации". Это неверно; оригинальный постер был совершенно корректен, чтобы поставить вопрос, как они.

Напомним, что ковариантное отображение - это отображение, которое сохраняет существование и направление некоторого другого отношения . Например, отображение типа T на тип IEnumerable<T> является ковариантным, поскольку оно сохраняет отношение совместимости присваивания. Если Tiger совместим с присвоением с Animal, преобразование под картой также сохраняется: IEnumerable<Tiger> совместимо с присвоением с IEnumerable<Animal>.

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

class B
{
    public virtual Animal M() {...}
}
class D : B
{
    public override Tiger M() {...}
}

Tiger совместим с назначением с 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-typed. Если вы приведете его к B, вы увидите свойство Animal-typed. В любом случае вы все равно получаете виртуальное поведение через защищенный член.

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

3 голосов
/ 03 декабря 2010

Может быть несколько проблем с тем, чего вы пытаетесь достичь.

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

Надеюсь, это поможет.

0 голосов
/ 30 мая 2016

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

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

0 голосов
/ 27 июня 2012

ДА !! Как это. Котла больше, чем можно было бы надеяться, но он работает. Трюк делается с помощью методов расширения. Он дозирует некоторые неприятные заклинания внутри, но представляет ковариантный интерфейс.

Смотри также: 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);
        }
    }
}
0 голосов
/ 03 декабря 2010

Одна идея без обобщений, но у нее есть и другие недостатки:

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), а затем выдает исключение.

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

0 голосов
/ 03 декабря 2010

Код, который вы разместили, не будет компилироваться, но я получаю базовое представление о том, что вы хотите сделать.Короче говоря, ответ - да, это единственный способ.Если вы хотите, чтобы свойство возвращало разные типы и печаталось по-разному в расширенных классах, вам следует использовать обобщенные значения так, как вы уже это делаете.Вы можете инкапсулировать публичный контракт объекта сотрудника, нового или существующего, в интерфейс, тогда вам не нужно вообще использовать дженерики.Вместо этого вы можете просто вернуть интерфейс и позволить полиморфизму вступить во владение.

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;
    }
}
0 голосов
/ 03 декабря 2010

Вы можете написать код для интерфейса сотрудника, чтобы получить то, что вы хотите, я думаю.

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> {} 
...