Интерфейсы, наследование, неявные операторы и преобразования типов, почему так?

Я работаю с библиотекой классов под названием DDay ICal. Это оболочка C# для системы iCalendar, реализованная в календарях Outlook и во многих других системах. Мой вопрос связан с некоторой работой, которую я выполнял с этой системой.

Здесь речь идет о 3 объектах

  • IRecurrencePattern — Интерфейс
  • RecurrencePattern — реализация интерфейса IRecurrencePattern
  • DbRecurPatt — пользовательский класс с оператором неявного типа.

IRecurrencePattern: показан не весь код

public interface IRecurrencePattern
{
    string Data { get; set; }
}

RecurrencePattern: показан не весь код

public class RecurrencePattern : IRecurrencePattern
{
    public string Data { get; set; }
}

DbRecurPatt: показан не весь код

public class DbRecurPatt
{
    public string Name { get; set; }
    public string Description { get; set; }

    public static implicit operator RecurrencePattern(DbRecurPatt obj)
    {
        return new RecurrencePattern() { Data = $"{Name} - {Description}" };
    }
}

Запутанная часть: в нашей системе DDay.ICal они используют ILists для хранения коллекции шаблонов повторения для каждого события в календаре, пользовательский класс используется для извлечения информации из базы данных, а затем он передается в шаблон повторения через оператор неявного преобразования типа.

Но в коде я заметил, что он продолжает падать при преобразовании в List<IRecurrencePattern> из List<DbRecurPatt>. Я понял, что мне нужно преобразовать в RecurrencePattern, а затем преобразовать в IRecurrencePattern (поскольку есть другие классы, которые реализуют IRecurrencePattern по-другому, которые также включены в коллекцию

var unsorted = new List<DbRecurPatt>{ new DbRecurPatt(), new DbRecurPatt() };
var sorted = unsorted.Select(t => (IRecurrencePattern)t);

Приведенный выше код не работает, выдает ошибку IRecurrencePattern.

var sorted = unsorted.Select(t => (IRecurrencePattern)(RecurrencePattern)t);

Это действительно работает, поэтому у меня есть вопрос; Почему первый не работает? (И есть ли способ улучшить этот метод?)

Я полагаю, что это может быть связано с тем, что неявный оператор находится в объекте RecurrencePattern, а не в интерфейсе, это правильно? (Я новичок в интерфейсах и неявных операторах)


person Alec Scratch    schedule 26.08.2015    source источник
comment
List<DbRecurPatt> — это реальный объект. Вы не можете привести его к List<IRecurrencePattern>, потому что кто-то может попытаться вставить в него не-DbRecurPatt.   -  person Damien_The_Unbeliever    schedule 26.08.2015
comment
Следует отметить, что приведение от RecurrencePattern к IRecurrencePattern будет относиться к одному и тому же объекту, но приведение от DbRecurPatt к RecurrencePattern создает совершенно новый объект. Итак, вам нужно сказать ему создать новый объект, прежде чем обращаться к нему как к интерфейсу.   -  person juharr    schedule 26.08.2015


Ответы (5)


Вы в основном попросили компилятор сделать это:

  1. У меня есть это: DbRecurPatt
  2. Я хочу это: IRecurrencePattern
  3. Пожалуйста, найдите способ добраться из пункта 1 в пункт 2.

Компилятор, даже если у него может быть только один выбор, не позволяет вам сделать это. Оператор приведения конкретно говорит, что DbRecurPatt можно преобразовать в RecurrencePattern, а не в IRecurrencePattern.

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

Поскольку не был определен оператор, позволяющий напрямую преобразовывать DbRecurPatt в IRecurrencePattern, компилятор скомпилирует это как жесткое приведение, переинтерпретировав ссылку как ссылку через интерфейс, что приведет к сбою во время выполнения.

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

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

Если вы попытаетесь определить такой оператор:

public static implicit operator IRecurrencePattern(DbRecurPatt obj)
{
    return new RecurrencePattern() { Data = $"{obj.Name} - {obj.Description}" };
}

Компилятор скажет следующее:

CS0552
'DbRecurPatt.implicit operator IRecurrencePattern(DbRecurPatt)': определяемые пользователем преобразования в интерфейс или из интерфейса не допускаются.

person Lasse V. Karlsen    schedule 26.08.2015
comment
Я думаю, что вы оставили I в имени интерфейса в примере кода неявного оператора. - person juharr; 26.08.2015
comment
Последняя часть этого утверждения; Вы имели в виду return new IRecurrencePattern() вместо return new RecurrencePattern, потому что при попытке использовать этот код он работает? - person Alec Scratch; 26.08.2015
comment
@AlecScratch Нет, я думаю, он имел в виду, что подпись должна быть public static implicit operator IRecurrencePattern(DbRecurPatt obj) - person juharr; 26.08.2015
comment
Ах хорошо, я вижу, что сейчас. В любом случае, это не имело бы смысла, потому что вы можете создать экземпляр интерфейса. Дерп. - person Alec Scratch; 26.08.2015
comment
Да, я пропустил букву I в подписи, я скопировал неправильный метод из моего примера программы LINQPad, но это правильно, что я хотел использовать класс внутри метода. - person Lasse V. Karlsen; 26.08.2015

Почему первый не работает?

Потому что вы запрашиваете у среды выполнения два неявных преобразования — одно в RecurrencePattern и одно в IRecurrencePattern. Среда выполнения будет искать только прямые неявные отношения — она не будет сканировать все возможные маршруты, чтобы вы попросили ее перейти. Предположим, что имело место множество неявных преобразований в классы разных типов, реализующие IRecurrencePattern. Какой из них выберет среда выполнения? Вместо этого он заставляет вас указывать отдельные приведения.

Это описано в разделе 6.4.3 спецификации языка C#:

Оценка определяемого пользователем преобразования никогда не включает более одного определяемого пользователем или поднятого оператора преобразования. Другими словами, преобразование из типа S в тип T никогда не будет сначала выполнять определяемое пользователем преобразование из S в X, а затем выполнять определяемое пользователем преобразование из X в T.

person D Stanley    schedule 26.08.2015

Как уже указывали другие, вы не можете сделать прямой переход от DbRecurPatt к IRecurrencePattern. Вот почему вы получаете этот очень уродливый двойной состав:

var sorted = unsorted.Select(t => (IRecurrencePattern)(RecurrencePattern)t);

Но, для полноты картины, следует упомянуть, что можно перейти от DbRecurPatt к IRecurrencePattern без каких-либо слепков с вашим текущим дизайном. Просто для этого вам нужно разбить выражение на несколько операторов, и при этом код становится значительно уродливее.

Тем не менее, приятно знать, что вы можете сделать это без приведения типов:

var sorted = unsorted.Select( t => {
    RecurrencePattern recurrencePattern = t; // no cast
    IRecurrencePattern recurrencePatternInterface = recurrencePattern; // no cast here either
    return recurrencePatternInterface;
});

ИЗМЕНИТЬ

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

var sorted = unsorted
    .Select<DbRecurPatt, RecurrencePattern>(t => t) // implicit conversion - no cast
    .Select<RecurrencePattern, IRecurrencePattern>(t => t); // implicit conversion - no cast
person sstan    schedule 26.08.2015
comment
Спасибо за ответ, но один вопрос, знаете ли вы, лучше ли это, чем метод приведения? Или это чисто полезно знать? - person Alec Scratch; 26.08.2015
comment
Есть одно преимущество (которое я лично считаю большим): если вы можете скомпилировать код без каких-либо приведений, то вы можете быть уверены, что преобразование не произойдет внезапно во время выполнения. Сравните это с вашим выражением (IRecurrencePattern)t, которое скомпилировалось, но не удалось во время выполнения. Но нельзя отрицать, что код уродливее и многословнее. - person sstan; 26.08.2015
comment
Там все еще есть два приведения, это просто неявные приведения, поскольку вы копируете ссылку из переменной одного типа в переменную другого типа. Так что говорить, что нет слепков, неправильно. - person D Stanley; 27.08.2015
comment
@D Стэнли: я понимаю, что ты говоришь. Но я думаю, что есть разница между приведением и конверсией. Если бы я сказал, что преобразование не требуется, я бы согласился с вами, что это было бы неправильно. Примеры такого использования терминологии в стандартных документах можно найти здесь (Преобразования, объявленные как явные, требуют вызова приведения.), здесь (неявное преобразование — приведение не требуется) или здесь - person sstan; 27.08.2015
comment
Ура! Людям нравится моя идея! Я думаю, что для ситуации с ОП сочетание двух методов окажется лучшим и наиболее элегантным. Честно говоря, пока избегают жестких бросков, OP должен быть хорошим. - person Bill Nadeau; 27.08.2015

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

var sorted = unsorted
   .Select<DbRecurPatt, RecurrencePattern>(t => t)
   .ToList<IRecurrencePattern>();

Вы также можете объединить этот ответ с ответом sstan, чтобы избежать дополнительного оператора Linq.

person Bill Nadeau    schedule 26.08.2015
comment
Мне очень нравится твоя идея. Я позаимствовал вашу идею и внес коррективы в свой ответ. По сути, я бы связал 2 вызова метода Select, чтобы избежать приведения, сохраняя при этом код, эквивалентный исходному фрагменту кода OP. Результат довольно чистый. - person sstan; 27.08.2015

... и чтобы ответить на ваш последний вопрос о неявном операторе - нет, вы не можете определить неявный оператор в интерфейсе. Эта тема более подробно рассматривается в этом вопросе:

неявный оператор с использованием интерфейсов

person olitee    schedule 26.08.2015