Комментарии 73
Там вообще жесть :)
Прям как с питоном ситуация.
Т.е. я создам интерфейс сразу с реализацией, потому что не хочу плодить лишние классы, а любой другой реализации надо будет переопределять метод?
Потому что множественное наследование запрещено. А методы по умолчанию позволяют расширять legacy API не ломая старый код
У интерфейса не может быть нестатических полей.
Думаю, отличается правилами вызова. Например:
- Один интерфейс с реализованным методом — класс использует реализацию интерфейса.
- Переопределяем метод у класса — класс использует свою реализацию. Каст к интерфейсу вызывает реализацию из интерфейса.
- Несколько интерфейсов с реализацией — работает как будто несколько интерфейсов реализованно явно.
Каст к интерфейсу вызывает реализацию из интерфейса.
Если бы все было именно так — то в этой фиче не было бы ни малейшего смысла, потому что сейчас именно так работают Extension Methods. Даже само название "Default Interface Methods" подразумевает, что реализация из интерфейса используется только если она не переопределена в классе.
Это нужно для той же цели, для которой сейчас делаются методы-расширения, но с возможностью переопределить такой метод для более эффективной реализации.
Посмотрите сколько явных проверок типа сделано в реализации Enumerable — каждую из них можно было бы заменить переопределением виртуальной функции. Не уверен насчет скорости работы — но расширяемее это бы получилось однозначно.
ИМХО, в интерфейсе не должно быть реализации
Хм, снова возвращаемся к проблеме ромбовидного наследовния? И для решения постоянно прийдётся писать explicit?
Отсутствие полей у интерфейсов, а также тот факт что реализация интерфейса — всегда виртуальная в терминах С++ снимают главные проблемы ромбовидного наследования. Остается только проблема конфликтов имен.
Остается только проблема конфликтов имен.
Ну так это одна из основополагающих и ключевых проблем, разве нет?
Если есть два интерфейса IA, IB с одинаково описанной функцией void Function(), но которая реализует разную функциональность, то
public class Test: IA, IB { /* без реализации - ведь она "больше не нужна" */ }
....
var a = new Test();
a.Function(); // <= функция какого интерфейса вызовется?
Если не добавлять новых методов в старые интерфейсы — то такую ситуацию должен отловить и исправить разработчик класса Test
.
К сожалению, одно из применений этой фичи — как раз добавление новых методов в существующие интерфейсы. И вот тут и правда может быть проблема.
Есть вариант реализовывать такие методы явно, т.о. не будет проблемы ромба.
Тогда ответом на вопрос "функция какого интерфейса вызовется?" — ошибка компиляции, если Function
явно не реализована в Test. А вызов Function
конкретного интерфейса будет доступен через каст.
Собственно в примере как раз это и происходит
IA i = new C();
Правда тогда не совсем ясна польза от таких финтов. Метод расширения в таком случае может быть предпочтительнее. Хотя это зависит конечно.
P.S. Немного погуглил и похоже так и планируется сделать.
Вот полный пример
interface IA
{
void M() { WriteLine("IA.M"); }
}
class C : IA { } // OK
IA i = new C();
i.M(); // prints "IA.M"
new C().M(); // error: class 'C' does not contain a member 'M'
Кстати там есть еще несколько интересных моментов. Обсуждают ряд вопросов, вроде статических, виртуальных или закрытых членов интерфейсов и т.д..
Не пугайте так! Очевидно же, что виртуальные члены в интерфейсах смысла не имеют (они же и так все по умолчанию виртуальные). А вот модификатор sealed разрешили, но не потому что он кому-то позарез нужен, а просто из желания не запрещать ничего лишнего.
Почитаю из дома....
Но тут как раз вопрос: зачем создавать проблему, а потом доблестно её преодолевать?!
Борьба с ветряными мельницами какая-то — сначала (в первой редакции языка C#) отказываемся от ромба и пафосно об этом заявляем, а, спустя немного времени, возвращаемся к исходному и уже начинаем воротить костыли на костыли, чтобы вернуть ромб и — пафосно заявляем, насколько крутую фичу добавили…
Вообщем — посмотрим когда фича выйдет и… воздержимся от использования… Во избежание. :D
Самый простой пример:
interface Eq {
bool Equal(a, b) { return !NotEqual(a, b);}
bool NotEqual(a, b) {return !Equal(a, b);}
}
Таким образом для реализации интерфейсов у нас минимальный набор операций для реализации это или Equal или NotEqual. В нек-ых случаях может оказаться, что операцию NotEqual реализовать проще чем Equal, а оставшуюся операцию мы получаем «бесплатно».
Во-первых, зачем нужен отдельный метод
NotEqual
, когда можно написать !Equal(a, b)
? Если вдруг проще вычислить неравенство, можно просто инвертировать это значение перед возвратом из метода.Во-вторых, добавляя такой интерфейс к классу, мы по умолчанию получаем возможность свалиться в бесконечную рекурсию. Компилятор уже не напомнит, что мы забыли реализовать «нормальное» сравнение объектов. Довольно дорогая цена за сомнительное удобство!
Методы для интерфейсов и сейчас без проблем добавляются через extensions-методы:
public interface IRectangle
{
int Width { get; }
int Height { get; }
}
public static class IRectangleExtensions
{
public static int GetArea(this IRectangle r) => r.Width * r.Height;
}
Как по мне, так вполне вменяемое применение. Просто упростят реализацию.
И будет логично, что переопределить метод интерфейса будет невозможно.
Ну почему же, к протоколам взаимодействия иногда прикладывают референсные реализации или типовые сценарии. Нет ничего плохого в том, что теперь их можно отразить и в языке.
Зайду с другой стороны.
В чем вы видите принципиальное отличие между двумя интерфейсами ниже и почему первый — это нормально, а второй — лютая жесть и поломанная концепция?
interface A {
void Foo(int x = 42);
}
interface B {
void Foo(int x);
void Foo() => Foo(42);
}
Вообще-то да. Более того, если класс используется через интерфейс, как часто бывает при использовании DI, то нигде кроме интерфейса писать = 42
не имеет смысла.
Если я в классе (в реализации интерфейса) установил другое значение, то теперь от типизации переменной будет зависеть какое значение придёт по умолчанию.
И это я молчу о потенциальной смене дефолтных значений, которое и для классов не работает без полной пересборки.
Тем что в первом случае вы говорите что есть интерфейс доступа к некоторой сущности. В этом интерфейсе обязан быть метод, который можно вызвать с параметром и без.
Во втором случае вы говорите тоже самое но с одним большим НО. Какого-то черта описание протокола взаимодействия вызывает какой-то метод. Ваш пример защищает концепт мой пример говорит что это кривая лажа:
interface A {
void Foo(int x = 42);
}
interface B {
void Foo(int x);
void Foo() { Foo(Unity.Resolve<DatabaseContext>().Set<Entity>().First(x => x.Id >=1000).Offset) };
}
Ваш пример говорит только он том, что он сам — кривая лажа.
Писать фигню можно на любом языке и с использованием любых инструментов, от написания фигни не застрахован ни один язык (кроме, возможно, HQ9+).
А потому оценивать языковые возможности надо по их лучшим проявлениям, а не по худшим.
Окей, убедите. Приведите пример вызова методов из интерфейсов, когда это было бы оправдано с точки зрения "было плохо, стало лучше" и оправдайте решение.
The principal motivations for this feature are
- Default interface methods enable an API author to add methods to an interface in future versions without breaking source or binary compatibility with existing implementations of that interface.
- The feature enables C# to interoperate with APIs targeting Android (Java) and iOS (Swift), which support similar features.
- As it turns out, adding default interface implementations provides the elements of the "traits" language feature (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Traits have proven to be a powerful programming technique (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).
— https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md#motivation
Например ToList()
который сейчас экстеншн метод к IEnumerable<>
. Сейчас если я делаю свой класс, имплементящий IEnumerable
, то метод ToList()
пройдет по моему IEnumerable<>
и если элементов много, то создаст в процессе много массивов, копируя их туда-сюда в процессе.
Если в моей реализации заранее известно количество элементов, то я мог бы в принципе сделать оптимальнее, только это очень убого будет. Нужно будет экстеншн MyToList()
делать, который проверит тип IEnumerable<>
, является ли он моей реализацией, вызовет оптимальную реализацию если да, или стандартную если нет.
Ботомлайн — эту фичу уже можно делать костыльно с помощью экстеншн методов. Те кому не нравится фича, должны ненавидеть и экстеншн методы, потому что они те же самые методы на интерфейсах, только хуже.
One of proposed syntaxes is to use existing in keyword as a shorter form of ref readonly. It could be that both syntaxes are allowed or that we pick just one of them.
string x = "demo";
Action y = () => M();
var t = (x: x, y);
t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда
Вообще, по-моему, это хороший рассадник граблей. Очень неявно и для неопытных программистов может быть сложно определить источник проблемы.
Мне кажется, что то же самое было бы с анонимным объектом (а-ля new { x, y }.y()
), так что это не новая грабля :)
Где-то читал, что в C# 7 хотели сделать not null reference types. Предлагали использовать
string! str1 = "123"; //not null
string! str2; //ошибка, not-null типу присваивается null
string str3; //стандартное поведение
Как я понял, это не сделали и это трансформировалось в Static null checking in C# в C# 8.
DefaultInterfaceMethods выглядят каким-то жутким хаком и нарушением логики — если это интерфейс, то он по-определению не должен содержать реализаций! Тогда это приведет к множественному наследованию со всеми вытекающими проблемами. Если есть необходимость во множественном наследовании, можно пойти путем, как многие языки — mixins. На мой взгляд лучше.
На самом деле, если это интерфейс, то он по определению не должен содержать полей. Default Interface Method принципиально ничем не отличается от пары из метода-расширения и доп. интерфейса, только пишется проще, а работает быстрее:
interface A {
void Foo(int x);
void Foo2() { Foo(2); }
}
// Почти эквивалентно
interface A {
void Foo(int x);
}
interface AExt {
void Foo2();
}
static class AExtStatic {
public static void Foo2(this A obj) {
var objext = obj as AExt;
if (objext == null)
obj.Foo(2);
else
objext.Foo2();
}
}
Трейты будут?
static Vector3 Add (ref readonly Vector3 v1, ref readonly Vector3 v2)
…
static Vector3 Add (in Vector3 v1, in Vector3 v2)
…
Почему in, ведь ref readonly понятнее? А потому что in короче.
Можно еще короче:
static Vector3 Add (=Vector3 v1, =Vector3 v2)
Немного непривычно, но глаз быстро привыкнет.
int $GeneratedMain(string[] args)
…
Я как и многие ожидал этот функционал еще в 7-ой версии языка.
А можно пример, зачем это может быть нужно?
Новые возможности C#, которые можно ожидать в ближайшее время