Подтвердить что ты не робот

Каковы веские причины, по которым разработчики .NET могут наследовать один из типичных типов параметров?

Этот пост является продолжением этого.

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

Задача состоит в том, чтобы собрать убедительные причины в пользу этой функции или, наоборот, узнать, что ее нет.

Я привожу свою причину, чтобы ответить на этот вопрос - см. Ниже.

Я прошу людей добавить их в качестве ответов на этот пост.

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

PS

Некоторые шаблоны C++ не имеют отношения к .NET. Например, в своей превосходной книге Modern C++ Design Андрей Александреску описывает, как создать список типов, оцененных во время компиляции. Естественно, этот шаблон не имеет значения для .NET, где, если мне нужен список типов, я просто создаю List<Type> и заполняю его типами. Итак, давайте попробуем найти причины, относящиеся к платформе .NET, а не просто слепо переводить методы кодирования C++ в С#.

PPS

Конечно, это обсуждение носит чисто академический характер. Даже если обнаружится сотня веских причин для рассматриваемой функции, она никогда не будет реализована.

4b9b3361

Ответ 1

Время от времени я натыкаюсь на проблему реализации, где я глубоко сожалею, что C<T> не может наследовать T. К сожалению, я никогда не записывал эти проблемы, и поэтому я могу описать только самую последнюю - ту, на которой я наткнулся прямо сейчас. Итак, вот оно:

Наша система расширяема через метаданные, которые становятся доступными во время выполнения. Метаданные преобразуются в тип, динамически генерируемый во время выполнения, используя Reflection.Emit. К сожалению, динамически сгенерированный тип должен удовлетворять следующим условиям:

  • Он должен быть получен из некоторого другого типа, предоставленного в качестве параметра для создателя динамического типа. Этот тип называется типом предка и всегда является статически скомпилированным типом.
  • Он должен реализовать несколько интерфейсов, например IDynamicObject (наш интерфейс), System.ComponentModel.INotifyPropertyChanged и Csla.Core.IMobileObject. Обратите внимание, что это условие накладывает определенные ограничения на тип предка. Например, тип предка может не реализовывать интерфейс IDynamicObject, за исключением того, что все методы интерфейса реализованы абстрактно. Существуют и другие ограничения, все из которых необходимо проверить.
  • Он должен (и он) переопределять методы object Equals, ToString и GetHashCode.

В настоящее время мне пришлось использовать Reflection.Emit для испускания всего кода IL для удовлетворения этих условий. Конечно, некоторые задачи могут быть перенаправлены на статически скомпилированный код. Например, переопределение метода object.Equals(object) вызывает DynamicObjectHelper(IDynamicObject, IDynamicObject), который является статически скомпилированным кодом С#, выполняющим большую часть работы сравнения двух динамических объектов для равенства. Но это скорее исключение, чем правило - большая часть реализации испускается, что является болью в прикладе.

Не было бы здорово написать типичный тип, например DynamicObjectBase<T>, который будет создан с типом предка и будет служить фактическим базовым типом динамически сгенерированного типа? Как я вижу, общий тип DynamicObjectBase<T> может реализовать большую часть требований динамического типа в статически скомпилированном С# -коде. Динамически испущенный тип наследует его и, вероятно, просто переопределяет несколько простых виртуальных методов.

В заключение, моя веская причина заключается в том, что позволить C<T> наследовать от T значительно упростит задачу испускания динамических типов.

Ответ 2

Я использую GTK # 3. Определяется класс Widget, который является базой для всех других виджетов GTK #, например. Окно, метка, рамка. К сожалению, структура немного усложняет изменение фонового цвета виджета в том смысле, что вам нужно переопределить метод виджета (jeez...).

Итак, если бы я хотел сказать - лейбл, для которого я могу произвольно установить фоновый цвет, я должен сделать что-то вроде этого:

class BGLabel : Label
{
    private Color _bgColor;

    public Color BackgroundColor
    {
        get { return this._bgColor; }
        set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
    }

    protected override void OnDraw(...)
    {
        ... /* here we can use _bgColor to paint the background */
    }
}

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

class C<T> : T where T : Widget
{
    private Color _bgColor;

    public Color BackgroundColor
    {
        get { return this._bgColor; }
        set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
    }

    protected override void OnDraw(...)
    {
        ... /* here we can use _bgColor to paint the background */
    }
}

и вместо "BGxxx bgw = new BGxxx();" Я мог бы использовать "C <xxx> bgw = new C <xxx> ();".

Ответ 3

Однажды, когда я хотел, это было, когда у меня были объекты базы данных (например, MyEntity), которые также имели соответствующий объект истории (например, MyEntityHistory). Эти объекты имеют одни и те же свойства, за исключением 2, которые присутствовали только в историческом: ValidFrom и ValidTo.

Итак, чтобы отобразить текущее состояние объекта, а также данные истории, я мог бы извлечь между ними этот записывающий UNION и использовать возможности отображения Dapper или EF Core FromSql чтобы отобразить результат в историю List<MyEntityHistory>.

Конечно, я бы хотел избежать дублирования свойств, поэтому в идеале я бы наследовал класс сущности, чтобы получить его свойства. Но я также хотел бы получить свойства ValidFrom и ValidTo от какого-то другого базового класса, поэтому я ValidTo бы с проблемой алмазов. Что могло бы помочь, так это то, что у меня был бы класс истории, определенный следующим образом:

class MyEntityHistory : HistoryEntity<MyEntity>

Где HistoryEntity будет определяться так:

class HistoryEntity<TEntity> : TEntity where TEntity: class
{
    public DateTime ValidFrom { get; set; }
    public DateTime ValidTo { get; set; }
}

К сожалению, такая реализация HistoryEntity<TEntity> невозможна.

Обратите внимание, что встраивание класса сущности (MyEntity) через композицию не является опцией, потому что Mapper Dapper или EF Core не сможет обойти вложенный объект.

Ответ 4

Пока я вижу, к чему вы клоните, это выглядит как частный случай более общей проблемы плохой поддержки построения типов с помощью композиции в .NET. Было бы достаточно для вас что-то вроде подхода, описанного в https://connect.microsoft.com/VisualStudio/feedback/details/526307/add-automatic-generation-of-interface-implementation-via-implementing-member?

Ответ 5

Основное правило дженериков, которое предотвращает это, - "содержимое родового типа должно быть четко определено на общем аргументе". Давайте посмотрим, как это относится к следующему коду:

public abstract class AbstractBase
{
    public abstract string MyMethod();
}

public class SomeType<T> : T
{
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<AbstractBase> does not implement AbstractBase.MyMethod
        SomeType<AbstractBase> b = new SomeType<AbstractBase>();
    }
}

Итак, мы пытаемся реализовать MyMethod():

public class SomeType<T> : T
{
    public override string MyMethod()
    {
        return "Some return value";
    }
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<string> does not inherit a virtual method MyMethod()
        SomeType<string> b = new SomeType<string>();
    }
}

Итак, давайте сделаем требование, чтобы T был получен из AbstractBase:

public abstract class DerivedAbstractBase : AbstractBase
{
    public abstract string AnotherMethod();
}

public class SomeType<T> : T
    where T : AbstractBase
{
    public override string MyMethod()
    {
        return "Some return value";
    }
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<DerivedAbstractBase> does not implement DerivedAbstractBase.AnotherMethod()
        SomeType<DerivedAbstractBase> b = new SomeType<DerivedAbstractBase>();
    }
}

Резюме:

К тому моменту, когда вы учитываете все ограничения в базовых типах, вы настолько ограничены, что вывод из общего параметра бессмыслен.

Ответ 6

Я попытаюсь объяснить простым примером, почему нам нужно наследование от общих типов.

Короче говоря, мотивация заключается в том, чтобы значительно облегчить разработку того, что я называю Compile Time Order Enforcement, которое очень популярно в ORM Framework.

Предположим, что мы строим базу данных.

Вот пример построения транзакции с такой картой:

public ITransaction BuildTransaction(ITransactionBuilder builder)
{
    /* Prepare the transaction which will update specific columns in 2 rows of table1, and one row in table2 */
    ITransaction transaction = builder
        .UpdateTable("table1") 
            .Row(12)
            .Column("c1", 128)
            .Column("c2", 256)
            .Row(45)
            .Column("c2", 512) 
        .UpdateTable("table2")
            .Row(33)
            .Column("c3", "String")
        .GetTransaction();

    return transaction;
}

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

    { 
        ITransaction transaction = builder
            .UpdateTable("table1") 
            .UpdateTable("table2")  /*INVALID ORDER: Table can't come after Table, because at least one Row should be set for previous Table */
    }
    // OR
    {
        ITransaction transaction = builder
            .UpdateTable("table1") 
                .Row(12)
                .Row(45) /* INVALID ORDER: Row can't come after Row, because at least one column should be set for previous row */
    }

Теперь давайте посмотрим на интерфейс ITransactionBuilder сегодня, БЕЗ наследования от generic, который обеспечит требуемый порядок во время компиляции:

    interface ITransactionBuilder
    {
        IRowBuilder UpdateTable(string tableName);
    }
    interface IRowBuilder
    {
        IFirstColumnBuilder Row(long rowid);
    }
    interface IFirstColumnBuilder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface INextColumnBuilder : ITransactionBuilder, IRowBuilder, ITransactionHolder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface ITransactionHolder
    {
        ITransaction GetTransaction();
    }
    interface ITransaction
    {
        void Execute();
    }

Как вы можете видеть, у нас есть 2 интерфейса для построителя столбцов "IFirstColumnBuilder" и "INextColumnBuilder", которые в действительности не являются обязательными, и помните, что это очень простой пример конечного автомата времени компиляции, а в более сложной проблеме - количество Необязательные интерфейсы будут резко расти.

Теперь предположим, что мы можем наследовать от дженериков и подготовить такие интерфейсы

interface Join<T1, T2> : T1, T2 {}
interface Join<T1, T2, T3> : T1, T2, T3 {}
interface Join<T1, T2, T3, T4> : T1, T2, T3, T4 {} //we use only this one in example

Затем мы можем переписать наши интерфейсы на более интуитивно понятный стиль и с помощью единого конструктора столбцов и не влияя на порядок

interface ITransactionBuilder
{
    IRowBuilder UpdateTable(string tableName);
}
interface IRowBuilder
{
    IColumnBuilder Row(long rowid);
}
interface IColumnBuilder
{
    Join<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue);
}
interface ITransactionHolder
{
    ITransaction GetTransaction();
}
interface ITransaction
{
    void Execute();
}

Итак, мы использовали Join <... > для объединения существующих интерфейсов (или "следующих шагов" ), что очень полезно для разработки конечных автоматов.

Конечно, эта конкретная проблема может быть решена путем добавления возможности в С# для "соединения" интерфейсов, но ясно, что если бы можно было наследовать от дженериков, проблема вообще не существовала, а также ясно, что компиляция приведение в порядок времени очень полезно.

BTW. Для синтаксиса типа

    interface IInterface<T> : T {}

Нет никаких случаев "что, если", кроме цикла наследования, которые могут быть обнаружены во время компиляции.

Я думаю, что AT LEAST для интерфейсов эта функция нужна на 100%

Привет

Ответ 7

Я написал CodeFirstWebFramework, который является dll, предоставляющим все инструменты для записи веб-сервера, управляемого базой данных, с таблицами автоматически генерируемые из классов в коде, и склеивание URL-адресов при вызове методов в классах AppModule.

DLL предоставляет некоторые базовые классы AppModule - AdminModule и FileSender. Администрирование этих приложений (вход в систему, поддержка пользователей, обновление настроек) и возврат файлов, когда URL-адрес не связан с каким-либо другим AppModule.

Потребители DLL могут переопределить абстрактный класс AppModule, чтобы добавить дополнительные функции для всех своих AppModules (которые будут получены из переопределения).

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

Я думал, что у меня была блестящая идея:

В моей DLL:

abstract class AppModule {
    // Contains base class implementation, including virtual Database property, code to 
    // retrieve files, code to call appropriate method depending on url, code to collect 
    // result and return it to the web browser, etc.
}

abstract class AdminModule<T> where T:AppModule, new() : T {
    // Contains implementation of Admin methods - create/edit users, 
    // edit settings, backup database, etc.
}

class AdminModule : AdminModule<AppModule> {
    // No code needed - inherits implementation from AdminModule<AppModule>
}

В потребительской программе (другое пространство имен):

abstract class AppModule : AppModule {
    // Contains additional properties and methods. Also overides some virtual methods,
    // e.g. returns a subclass of Database with additional methods, or retrieve files
    // from the database instead of the file system.
}

class AdminModule : AdminModule<AppModule> {
    // Additional code for Admin methods specific to this application
    // Does not need code for methods in base class - inherits implementation from 
    // AdminModule<AppModule>
}

В этом конкретном случае из-за ограничения, что T является AppModule, и потому, что AdminModule <T> сам по себе абстрактный, я не вижу причин, почему какое-либо из возражений, поднятых до сих пор, будет применяться.

Компилятор может проверить, что любые абстрактные методы были реализованы в производных классах. Не было бы никакой опасности создавать AdminModule <T> , потому что он является абстрактным.

Было бы ошибкой времени компиляции писать AdminModule <T> если T был запечатанным классом или получен из AdminModule <T> , или имеет меньшую доступность, чем AdminModule <T> .

Ответ 8

Хорошая причина сделать возможным наследование от параметра типа из-за следующей ситуации:

Я хотел бы иметь абстрактный класс ViewModel, который наследуется от класса Entity и имеет некоторые дополнительные свойства и методы.

public abstract class ViewModel<TEntity> : TEntity, IViewModel<TEntity> 
where TEntity : class, IEntity, new() {
    public void SomeMethod() {

    }
}

Тогда я мог бы сделать конкретную ViewModel

public class EmployeeViewModel : ViewModel<Employee> {

}

Отлично! он наследует все поля сущности и имеет стандартные свойства и методы абстрактной модели представления!..

К сожалению, этого нельзя сделать сейчас.