В апреле 2003-его года был выпущен C# 1.2 и с тех пор все версии имели только major версию.
И вот сейчас, если верить официальной страничке roslyn на github, в работе версии 7.1 и 7.2.
Для того, чтобы попробовать версию 7.1 необходимо установить pre-release Visual Studio. Скачать ее можно скачать с официального сайта
Зайдя в свойства решения можно выбрать используемую версию языка.
Далее я хочу рассказать о понравившихся мне новых фичах языка.
Асинхронный Main (планируется в версии 7.1)
В наше время очень часто встречаются программы, которые почти полностью асинхронные.
До сих пор Main мог быть void или int. И мог содержать в себе аргументы в виде массива строк. Сейчас добавятся несколько новых перегрузок:
public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);
Так как CLR не поддерживает асинхронные entrypoints, то компилятор сам создает boilerplate code (то есть автоматически вставляет какой-то дополнительный код, который позволяет привести void к Task)
Примерно такой код будет сгенерирован компилятором:
async Task<int> Main(string[] args) {
// здесь какой-то ваш код
}
// этот метод сгенерируется «за кулисами» автоматически
int $GeneratedMain(string[] args) {
return Main(args).GetAwaiter().GetResult();
}
Я как и многие ожидал этот функционал еще в 7-ой версии языка.
Литерал default (планируется в версии 7.1)
У Visual Basic и у C# возможности примерно одинаковые. Но в самих языках есть определенные различия. Скажем у C# есть null, а у VB.NET есть Nothing. Так вот, Nothing может конвертироваться в любой системный тип, представляя собой значение по умолчанию для этого типа. Понятно, что null этого не делает. Хотя, значением по умолчанию вполне может быть и null.
Сейчас уже есть и применяется выражение default(T). Рассмотрим пример его применения. Допустим, что у нас есть метод, который принимает массив строк в качестве параметра:
void SomeMethod(string[] args)
{
}
Можно выполнить метод и передать ему значение массива строк по умолчанию так:
SomeMethod(default(string[]));
Но зачем писать default(string[]), если можно просто написать default?
Вот, начиная с C# 7.1 и можно будет пользоваться литералом default. Что еще можно будет с ним сделать, кроме как передать значение по умолчанию в метод? Например, его можно использовать в качестве значения необязательного параметра:
void SomeMethod(string[] args = default)
{
}
или инициализировать переменную значением по умолчанию:
int i = default;
А еще можно будет проверить не является ли текущее значение значением по умолчанию:
int i = 1;
if (i == default) { } // значением по умолчанию типа int является 0
if (i is default) { } // точно такая же проверка
Что нельзя будет сделать? Следующие примеры того как литерал default использовать нельзя:
const int? y = default;
if (default == default)
if (default is T) // оператор is нельзя использовать с default
var i = default
throw default
default as int; // 'as' может быть только reference тип
Readonly ref (планируется в версии 7.2)
При отправке структур в методы в качестве by value (по значению) происходит копирование объекта, что стоит ресурсов. А иногда бывает необходимость отправить только значение ссылочного объекта. В таком случае разработчики отправляют значение по ссылке ref для того, чтобы сэкономить память, но при этом пишут в комментариях что значение изменять нельзя. Особенно часто это происходит при математических операциях с объектами vector и matrix.
Понятный пример того что нас ждет:
static Vector3 Add (ref readonly Vector3 v1, ref readonly Vector3 v2)
{
// так нельзя!
v1 = default(Vector3);
// и так нельзя!
v1.X = 0;
// так тоже нельзя!
foo(ref v1.X);
// а вот теперь нормально
return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
Эта функция пока что в состоянии прототипа, но я уверен в ее необходимости. Одним из предлагаемых вариантов является использование ключевого слова in вместо ref readonly
static Vector3 Add (in Vector3 v1, in Vector3 v2)
{
return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}
Почему in, ведь ref readonly понятнее? А потому что in короче.
Методы интерфейсов по умолчанию (планируется в версии 8.0)
Эти методы мы увидим не так скоро.
Представьте себе, что есть какой-то класс или структура, которые реализуют интерфейс. И вот они смогут унаследовать реализацию одного метода из интерфейса(!) или должны будут реализовать свою версию этого метода.
Уже само понятие реализации в интерфейсе звучит довольно странно. Хотя у Java 8 есть методы интерфейсов по умолчанию, и разработчики использующие C# тоже захотели себе подобный функционал.
Этот функционал позволит автору API изменить код метода для всех уже существующих реализаций интерфейса.
На примере должно быть понятно. Допустим у нас есть такой вот интерфейс у которого метод SomeMethod содержит реализацию:
interface IA
{
void SomeMethod() { WriteLine("Вызван SomeMethod интерфейса IA"); }
}
Теперь создадим класс, которые реализует интерфейс:
class C : IA { }
И создадим экземпляр интерфейса:
IA i = new C();
Теперь мы можем вызвать метод SomeMethod:
i.SomeMethod(); // выведет на экран "Вызван SomeMethod интерфейса IA"
Предположительные имена кортежей (планируется в версии 7.1)
Этот функционал позволит в некоторых случаях не указывать имена кортежей. При определенных условиях элементы кортежа могут получить предположительные имена.
Пример:
Вместо того, чтобы писать (f1: x.f1, f2: x?.f2) можно просто написать (x.f1, x?.f2).
Первый элемент кортежа в таком случчае получит имя f1, а второй f2
Сейчас же кортеж стал бы неименнованным (к элементам которого можно обратиться только по item1, item2 ...)
Предположительные имена особенно удобны при использовании кортежей в LINQ
// c и result содержат в себе элементы с именами f1 и f2
var result = list.Select(c => (c.f1, c.f2)).Where(t => t.f2 == 1);
Отрицательные стороны
Основным недостатком введения этого функционала является совместимость с C# 7.0. Пример:
Action y = () => M();
var t = (x: x, y);
t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда
Но с этим небольшим нарушением совместимости можно смириться, так как период между выходом 7.0 и 7.1 будет небольшим.
Собственно, уже сейчас Visual Studio при использовании 7.0 предупреждает, что
Tuple element name 'y' is inferred. Please use language version 7.1 or greater to access an element by its inferred name.
Полный код примера:
class Program
{
static void Main(string[] args)
{
string x = "demo";
Action y = () => M();
var t = (x: x, y);
t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда
}
private static void M()
{
Console.WriteLine("M");
}
}
public static class MyExtensions
{
public static void y(this (string s, Action a) tu)
{
Console.WriteLine("extension method");
}
}
Если вы используете .NET 4.6.2 и ниже, то для того чтобы попробовать пример вам необходимо установить NuGet пакет System.ValueTuple.
Комментарии (68)
MonkAlex
27.06.2017 08:18+8А зачем нужны методы интерфейса с собственной реализацией?
Т.е. я создам интерфейс сразу с реализацией, потому что не хочу плодить лишние классы, а любой другой реализации надо будет переопределять метод?Illivion
27.06.2017 08:42+5И почему бы не использовать для этого абстрактный класс с виртуальными методами…
Mishkun
27.06.2017 13:37+4Потому что множественное наследование запрещено. А методы по умолчанию позволяют расширять legacy API не ломая старый код
svekl
27.06.2017 13:37+1Нельзя наследоваться от двух или более абстрактных классов, сейчас самое близкое к тому, что это даст — это два интерфейса с методами расширения.
futureader
27.06.2017 17:44-3Потому что dependency injection на абстрактном классе не сделать
denismaster
27.06.2017 22:02Можно и на абстрактном классе. У М.Симана в его книжке про DI в .NET об этом есть сноска.
Serg046
28.06.2017 16:44+1Как минимум, чисто технически, можно и обычный неабстрактный класс внедрять.
NeoCode
27.06.2017 09:21+2Интересно чем интерфейсы с реализацией методов будут отличаться от множественного наследования C++?
indestructable
27.06.2017 12:18-2Думаю, отличается правилами вызова. Например:
- Один интерфейс с реализованным методом — класс использует реализацию интерфейса.
- Переопределяем метод у класса — класс использует свою реализацию. Каст к интерфейсу вызывает реализацию из интерфейса.
- Несколько интерфейсов с реализацией — работает как будто несколько интерфейсов реализованно явно.
mayorovp
27.06.2017 12:21Каст к интерфейсу вызывает реализацию из интерфейса.
Если бы все было именно так — то в этой фиче не было бы ни малейшего смысла, потому что сейчас именно так работают Extension Methods. Даже само название "Default Interface Methods" подразумевает, что реализация из интерфейса используется только если она не переопределена в классе.
AndreiShenets
27.06.2017 09:21+8Выглядит как будто кто-то хочет протянуть аналог множественного наследования
kaiZer_dragomir
27.06.2017 14:41+1хотя отсутствие мультинаследования — именно одно из тех ограничений, которые помогают часто писать более чистый, понятный и масштабируемый код
mayorovp
27.06.2017 09:52+2Это нужно для той же цели, для которой сейчас делаются методы-расширения, но с возможностью переопределить такой метод для более эффективной реализации.
Посмотрите сколько явных проверок типа сделано в реализации Enumerable — каждую из них можно было бы заменить переопределением виртуальной функции. Не уверен насчет скорости работы — но расширяемее это бы получилось однозначно.
Shatun
27.06.2017 12:25+3интерфейс с собственной реализацией я думаю больше нужен самим авторам с#- чтобы легко добавлять новый функционал к старым интерфейсам и не переписывать при этом все библиотеки их использующие.
ИМХО, в интерфейсе не должно быть реализации
White_Scorpion
27.06.2017 12:29Хм, снова возвращаемся к проблеме ромбовидного наследовния? И для решения постоянно прийдётся писать explicit?
mayorovp
27.06.2017 12:34+1Отсутствие полей у интерфейсов, а также тот факт что реализация интерфейса — всегда виртуальная в терминах С++ снимают главные проблемы ромбовидного наследования. Остается только проблема конфликтов имен.
wheercool
27.06.2017 12:36+2Я думаю это попытка воплотить typeclass из Haskell.
Самый простой пример:
interface Eq { bool Equal(a, b) { return !NotEqual(a, b);} bool NotEqual(a, b) {return !Equal(a, b);} }
Таким образом для реализации интерфейсов у нас минимальный набор операций для реализации это или Equal или NotEqual. В нек-ых случаях может оказаться, что операцию NotEqual реализовать проще чем Equal, а оставшуюся операцию мы получаем «бесплатно».impwx
27.06.2017 14:57+2Имхо это плохой пример.
Во-первых, зачем нужен отдельный методNotEqual
, когда можно написать!Equal(a, b)
? Если вдруг проще вычислить неравенство, можно просто инвертировать это значение перед возвратом из метода.
Во-вторых, добавляя такой интерфейс к классу, мы по умолчанию получаем возможность свалиться в бесконечную рекурсию. Компилятор уже не напомнит, что мы забыли реализовать «нормальное» сравнение объектов. Довольно дорогая цена за сомнительное удобство!0xd34df00d
28.06.2017 22:11Пример действительно плохой, со сравнениями всё слишком тривиально.
Лучше рассмотреть, скажем, тайпкласс для монад (для некоторых удобнее написать
join :: m (m a) -> m a
, для некоторых —bind
aka>>= :: m a -> (a -> m b) -> m b
. Или, скажем, Traversable, или вообще Foldable, в котором вообще мясо, и минимальным необходимым определением является одна из двух функций, остальную дюжину функций этого тайпкласса можно вывести из них.
ZerGer
27.06.2017 13:36Методы для интерфейсов и сейчас без проблем добавляются через 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; }
Как по мне, так вполне вменяемое применение. Просто упростят реализацию.
И будет логично, что переопределить метод интерфейса будет невозможно.wheercool
27.06.2017 13:51+4И будет логично, что переопределить метод интерфейса будет невозможно
Как раз-таки в этом и вся соль, что можно переопределить метод по умолчанию.
IL_Agent
27.06.2017 22:53+1Получается, мы делаем интерфейс и сразу extension метод к нему, но который можно переопределять в реализациях… Функционально, но засоряет интерфейс, на мой взгляд. Лучше б какие-нибудь виртуальные extension методы запилили.
sidristij
28.06.2017 09:05+1Вопрос даже не в том что зачем метод интерфейсу… Вопрос в том что они ломают концепцию того что интерфейс по своей сути — контракт. Описание протокола взаимодействия. Не может быть в описании реализация. Они ломают концепцию. На мой взгляд, это лютая жесть
mayorovp
28.06.2017 09:14Ну почему же, к протоколам взаимодействия иногда прикладывают референсные реализации или типовые сценарии. Нет ничего плохого в том, что теперь их можно отразить и в языке.
sidristij
28.06.2017 10:31Абстрактные классы? Описание протокола взаимодействия — это, считай, документация ожиданий в реализации. Не более того
mayorovp
28.06.2017 10:35Зайду с другой стороны.
В чем вы видите принципиальное отличие между двумя интерфейсами ниже и почему первый — это нормально, а второй — лютая жесть и поломанная концепция?
interface A { void Foo(int x = 42); } interface B { void Foo(int x); void Foo() => Foo(42); }
MonkAlex
28.06.2017 11:40А что, можно в первом случае = 42 написать в интерфейс?
mayorovp
28.06.2017 12:11Вообще-то да. Более того, если класс используется через интерфейс, как часто бывает при использовании DI, то нигде кроме интерфейса писать
= 42
не имеет смысла.MonkAlex
28.06.2017 12:40Ну, довольно подозрительное поведение, на мой взгляд. Как впрочем и везде с дефолтными аргументами.
Если я в классе (в реализации интерфейса) установил другое значение, то теперь от типизации переменной будет зависеть какое значение придёт по умолчанию.
И это я молчу о потенциальной смене дефолтных значений, которое и для классов не работает без полной пересборки.
sidristij
28.06.2017 12:23Тем что в первом случае вы говорите что есть интерфейс доступа к некоторой сущности. В этом интерфейсе обязан быть метод, который можно вызвать с параметром и без.
Во втором случае вы говорите тоже самое но с одним большим НО. Какого-то черта описание протокола взаимодействия вызывает какой-то метод. Ваш пример защищает концепт мой пример говорит что это кривая лажа:
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) }; }
mayorovp
28.06.2017 12:27Ваш пример говорит только он том, что он сам — кривая лажа.
Писать фигню можно на любом языке и с использованием любых инструментов, от написания фигни не застрахован ни один язык (кроме, возможно, HQ9+).
А потому оценивать языковые возможности надо по их лучшим проявлениям, а не по худшим.
sidristij
28.06.2017 12:49Окей, убедите. Приведите пример вызова методов из интерфейсов, когда это было бы оправдано с точки зрения "было плохо, стало лучше" и оправдайте решение.
mayorovp
28.06.2017 12:52+1The 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
Atomosk
30.06.2017 11:51+2Например
ToList()
который сейчас экстеншн метод кIEnumerable<>
. Сейчас если я делаю свой класс, имплементящийIEnumerable
, то методToList()
пройдет по моемуIEnumerable<>
и если элементов много, то создаст в процессе много массивов, копируя их туда-сюда в процессе.
Если в моей реализации заранее известно количество элементов, то я мог бы в принципе сделать оптимальнее, только это очень убого будет. Нужно будет экстеншнMyToList()
делать, который проверит типIEnumerable<>
, является ли он моей реализацией, вызовет оптимальную реализацию если да, или стандартную если нет.
Ботомлайн — эту фичу уже можно делать костыльно с помощью экстеншн методов. Те кому не нравится фича, должны ненавидеть и экстеншн методы, потому что они те же самые методы на интерфейсах, только хуже.
IL_Agent
28.06.2017 10:49+1Следует рассматривать методы с реализацией не как часть протокола, а как заранее определенную возможность работы с протоколом ( как extension method), которую можно переопредлить для конкретных имплементаций.
low_noize
27.06.2017 09:12+7«Почему in, ведь ref readonly понятнее? А потому что in короче.» Уверены?) а можем, потому что in — противоположность out?) «Передан по ссылке БЕЗ возможности записи» против «Передан по ссылке ОБЯЗАТЕЛЬНО для записи»
asommer
27.06.2017 09:12One 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.
razon
27.06.2017 09:19+2
alex1t
27.06.2017 10:01+2string x = "demo"; Action y = () => M(); var t = (x: x, y); t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда
Вообще, по-моему, это хороший рассадник граблей. Очень неявно и для неопытных программистов может быть сложно определить источник проблемы.ForNeVeR
27.06.2017 10:15Мне кажется, что то же самое было бы с анонимным объектом (а-ля
new { x, y }.y()
), так что это не новая грабля :)
impwx
27.06.2017 11:21Имхо, тут проблема не в новичках, а в нарушении обратной совместимости. На C# пишется много крупных enterprise-проектов, для которых обновление компилятора с возможностью незаметно сломать логику — слишком большой риск.
Alex_ME
27.06.2017 11:36-1Где-то читал, что в 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. На мой взгляд лучше.
mayorovp
27.06.2017 12:03+2На самом деле, если это интерфейс, то он по определению не должен содержать полей. 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(); } }
Danov
28.06.2017 22:09-2static 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)
Немного непривычно, но глаз быстро привыкнет.
xystarcha
05.07.2017 13:26int $GeneratedMain(string[] args)
…
Я как и многие ожидал этот функционал еще в 7-ой версии языка.
А можно пример, зачем это может быть нужно?mayorovp
05.07.2017 13:58+1Чтобы не писать одну и ту же обертку над Main каждый раз, когда есть необходимость использовать асинхронный код в основном потоке консольного приложения.
AndrewN
Язык развивается, это всегда приятно. С другой стороны, я рад что начал изучать C# достаточно давно и теперь все эти новые фичи поступают малыми порциями и легко усваиваются. А каково будет только начинающему изучать язык через пару лет новичку? Он захлебнется в куче этих фич… Да даже если я сейчас начну изучать тот же С++, там та же картина? Или я заблуждаюсь?
AgentFire
Там вообще жесть :)
alexeykuzmin0
Насколько я могу судить, в c++ еще хуже
Flowneee
Да ладно, стандарты выходят раз в три (?) года, да и чем-то прям глобальным был только 11й, осилить несколько фич раз в три года не настолько невозможно же, да?
alexeykuzmin0
В онгоуинге освоить вообще нормально, и даже не хватает, приходится boost использовать и иже с ним.
А вот осваивать современный c++ с нуля, мне кажется, жесть
mayorovp
Hydro
В точку.
Прям как с питоном ситуация.
Idot
А что с Питоном?
Yngvie
Ну тоже появляются новые вещи, в том числе и в синтаксисе. Всякие yield from, f-strings, async/await, dict literals, raise from.
И теперь придется учить больше, зато всяких костылей теперь меньше.
waverunner
Да, а что с питоном?