Scheduled New C # 8.0 Features

Original author: Damir Arh
  • Transfer


All the tools previously presented in minor C # versions are designed not to change the language too much. They are rather syntactic improvements and small additions to the new features of C # 7.0 .


This approach was deliberate, and it remains valid.


More serious changes that require more work at all stages of development (design, implementation and testing) will continue to be released only with major releases of the language. And although the final minor version of C # 7 has not yet been released, the team is already actively working on the following main version of the language: C # 8.0.


In this article, I plan to present a selection of currently planned functions that are likely to be in the final release. All of them are in the early stages of development and are subject to change.


Nullable Reference Types


This tool has already been proposed in the early stages of developing C # 7.0, but was postponed until the next major release. Its purpose is to help the developer avoid unhandled NullReferenceException exceptions.


The main idea is to allow when determining the type of a variable to indicate whether it can be null or not:


IWeapon? canBeNull;
IWeapon cantBeNull;

Assigning null or a value that could potentially contain null in a non-null variable will result in a compiler warning (which can be configured to interrupt further compilation if special security is needed):


canBeNull = null;       // нет предупреждения
cantBeNull = null;      // предупреждение
cantBeNull = canBeNull; // предупреждение

Similarly, warnings will be generated when dereferencing nullable variables without first checking for null:


canBeNull.Repair();       // предупреждение
cantBeNull.Repair();      // нет предупрежденияif (canBeNull != null) {
    cantBeNull.Repair();  // нет предупреждения
}

Translator's Note

Выглядит красиво, пока мы не вспоминаем про многозадачное окружение – как быть там в случае, если кто-то вклинился между проверкой и использованием?


The problem with this change is that we are interrupting compatibility with the old code base: It is assumed here that all variables in the old code cannot contain null by default. To cope with this situation, static analyzers of the compiler for null-security must be selectively enabled depending on the project.


A developer can turn on nullability when ready to deal with the resulting warnings. This is in the interests of the developer, since these warnings potentially indicate errors in the code.


An early preview of this feature is available for download in Visual Studio 2017 15.6 update .


Records


Currently, much of the C # boiler-code is written during the creation of a simple class that works as a container of values ​​and does not have any methods or business logic.


The syntax of the records should standardize the implementation of such classes with an absolute minimum of code:


public class Sword(int Damage, int Durability);

This string is equivalent to the following class:


publicclassSword : IEquatable<Sword>
{
    publicint Damage { get; }
    publicint Durability { get; }
    publicSword(int Damage, int Durability)
    {
        this.Damage = Damage;
        this.Durability = Durability;
    }
    publicboolEquals(Sword other)
    {
        return Equals(Damage, other.Damage) 
            && Equals(Durability, other.Durability);
    }
    publicoverrideboolEquals(object other)
    {
        return (other as Sword)?.Equals(this) == true;
    }
    publicoverrideintGetHashCode()
    {
        return (Damage.GetHashCode() * 17 + Durability.GetHashCode());
    }
    publicvoidDeconstruct(outint Damage, outint Durability)
    {
        Damage = this.Damage;
        Durability = this.Durability;
    }
    public Sword With(int Damage = this.Damage, int Durability 
        = this.Durability) => new Sword(Damage, Durability);
}

As you can see, this class contains read-only properties and a constructor for initializing them. It implements a comparison by value and correctly overlaps GetHashCode for use in hash collections such as Dictionary and Hashtable. There is even a Deconstruct method for deconstructing a class into individual values ​​using the tuple syntax:


var (damage, durability) = sword;

You probably did not recognize the syntax used in the last class method. Method default parameters additionally allow to refer to a field or class property using this syntax. This is particularly useful for implementing With helper, which is designed to create modified copies of existing immutable objects:


var strongerSword = sword.With(Damage: 8);

Additionally, the following syntactic sugar is considered for working with this method:


var strongerSword = sword with { Damage = 8 };

Recursive patterns


Some pattern matching features have already been added to C # 7.0. In version 8.0 it is planned to expand them with the following cases:


Recursive patterns will allow deconstructing matched types into a single expression. This should work well with the compiler-generated method for the Deconstruct () records:


if (sword isSword(10, var durability)) {
    // код выполняется, если Damage = 10// durability равно значению sword.Durability
}

A tuple pattern (tuple) will allow you to match more than one value in one expression:


switch (state, transition)
{
    case (State.Running, Transition.Suspend):
        state = State.Suspended;
        break;
}

The switch statement will allow compressed syntax in which the pattern matching result will be assigned to one variable. This syntax is still in development, but the current sentence looks like this:


state = (state, transition) switch {
    (State.Running, Transition.Suspend) => State.Suspended,
    (State.Suspended, Transition.Resume) => State.Running,
    (State.Suspended, Transition.Terminate) => State.NotRunning,
    (State.NotRunning, Transition.Activate) => State.Running,
    _ => thrownew InvalidOperationException()
};

Default Interface Methods


Currently, C # does not allow interfaces to contain implementations of methods, only their declarations:


interfaceISample
{
    voidM1();                                    // разрешеноvoidM2() => Console.WriteLine("ISample.M2"); // запрещено
}

To implement this functionality, abstract classes can be used instead:


abstractclassSampleBase
{
    publicabstractvoidM1();
    publicvoidM2() => Console.WriteLine("SampleBase.M2");
}

Despite this, in C # 8 it is planned to add support for default interface methods, using the syntax suggested in the first example. This will make it possible to implement scripts that are not supported by abstract classes.


The author of the library can extend the existing interface using the default method implementation. This will make it possible not to change the existing classes that worked with the older version of the interface. If they do not implement the new method, they will be able to use its default implementation. If you need to change the behavior, they can override it. There is no need to change the old code just because the interface has been expanded.


Multiple inheritance is forbidden in C #, so a class can be inherited from only one base abstract class.


In this case, a class can implement several interfaces. If these interfaces implement default interface methods, this allows classes to combine behavior from several different interfaces — this concept is known as trait and is available in many programming languages.


Unlike multiple inheritance, this functionality allows you to avoid the problem of diamond-shaped inheritance , in which a method with the same name is defined in several interfaces. For this, C # 8.0 requires that each class and interface have the most specific redefinition of each inherited member:


  • When a member with the same name is inherited from several interfaces, one override is more specific than the other if it is inherited from the other.
  • If none of the interfaces is directly or indirectly inherited from another interface, the developer must explicitly specify the override that he wants to use or write his own. Thus, it will resolve the ambiguity.

Asynchronous streams


C # already has support for iterators and asynchronous methods. In C # 8.0, it is planned to combine this pair into asynchronous iterators. They will be based on the asynchronous versions of the IEnumerable and IEnumerator interfaces:


publicinterfaceIAsyncEnumerable<outT>
{
    IAsyncEnumerator<T> GetAsyncEnumerator();
}
publicinterfaceIAsyncEnumerator<outT> : IAsyncDisposable
{
    Task<bool> MoveNextAsync();
    T Current { get; }
}

Also, consumers of the asynchronous version of iterators will need the asynchronous version of IDisposable:


publicinterfaceIAsyncDisposable
{
    Task DisposeAsync();
}

This will use the following code to iterate over the elements:


var enumerator = enumerable.GetAsyncEnumerator();
try
{
    while (await enumerator.WaitForNextAsync())
    {
        while (true)
        {
            Use(enumerator.Current);
        }
    }
}
finally
{
    await enumerator.DisposeAsync();
}

Translator's Note

Примечание переводчика – не понял, зачем здесь цикл while(true). Или я чего-то не понял, или так возникнет бесконечный цикл. Подозреваю, что это просто опечатка, но мало ли. Оставил, как в оригинальной статье, которая прошла техническую рецензию по месту оригинальной публикации.


This code is very similar to the one we already use for working with ordinary synchronous iterators. It looks unusual, because we usually use the expression foreach instead. The asynchronous version of the expression will work with asynchronous iterators:


foreachawait (var item in enumerable)
{
    Use(item);
}

As in the foreach expression, the compiler will generate the necessary code on its own.


It will also be possible to implement asynchronous iterators using the yield keyword. Approximately in the same style as it is done with synchronous iterators:


async IAsyncEnumerable<int> AsyncIterator()
{
    try
    {
        for (int i = 0; i < 100; i++)
        {
            yieldawaitGetValueAsync(i);
        }
    }
    finally
    {
        await HandleErrorAsync();
    }
}

Additionally, support for cancellation tokens and LINQ is expected.


Ranges


It is planned to add a new syntax for the range of values:


var range = 1..5;

This will create a structure representing the range:


struct Range : IEnumerable<int>
{
    publicRange(int start, int end);
    publicint Start { get; }
    publicint End { get; }
    public StructRangeEnumerator GetEnumerator();
    // overloads for Equals, GetHashCode...
}

This new type can be effectively used in several different contexts:


  • It can occur as an argument in indexers to provide a more compact syntax for array slices:

Span<T> this[Range range]
{
    get
    {
        return ((Span<T>)this).Slice(start: range.Start, 
            length: range.End - range.Start);
    }
}

  • Since this structure implements the IEnumerable interface, it can be used as an alternative syntax for iterating over a range of values:

foreach (var index in min..max)
{
    // обработка значений
}

  • Pattern matching can also use this syntax to determine that a value lies within a specified range:

switch (value)
{
    case1..5:
        // значение в диапазонеbreak;
}

Translator's Note

В первых двух сценариях неплохо было бы и аргумент step предусмотреть – такие сценарии встречаются не слишком часто, но регулярно. А еще – открытые диапазоны (для первого и третьего сценариев). Тогда диапазоны будут лучше соответствовать слайсам того же Python, что положительно скажется на унификации языковых средств.


It is still not decided whether the range operator will include or exclude, that is, whether the resulting range will include the end value. Both syntaxes may be available.


Translator's Note

Если будет принято решение оставить один вариант, то я бы предпочел тот, что в питоне, т.е.:


a[start:end] # значения от start до end-1
a[:end]      # items от начала до end-1

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


Generic Attributes


Support for generics in attributes will make it easier to implement type passing as an argument. Currently, it can be passed to attributes using the following syntax:


publicclassTypedAttribute : Attribute
{
    publicTypedAttribute(Type type)
    {
        // ...
    }
}

With generic support, a type can be passed as a generic argument:


publicclassTypedAttribute<T> : Attribute
{
    publicTypedAttribute()
    {
        // ...
    }
}

Apart from the fact that this syntax is more pleasant, it would also allow checking the type of the attribute argument:


publicTypedAttribute(T value)
{
    // ...
}

Default Literal in Deconstruction


To assign default values ​​to all members of a tuple in C # 7, you had to use the following syntax:


(int x, int y) = (default, default);

With the introduction of support for the default literal, the syntax of a similar expression can be simplified to:


(int x, int y) = default;

Caller Argument Expression


Starting in C # 5, the attributes CallerMemberName, CallerFilePath and CallerLineNumber are introduced, which make it easier to work with call point information for diagnostic purposes.


The CallerMemberName attribute also proved very useful for implementing the INotifyPropertyChanged interface:


Translator's Note

Но до удобства PropertyChanged.Fody ему далеко.


classViewModel : INotifyPropertyChanged
{
    publicevent PropertyChangedEventHandler PropertyChanged;
    privatevoidOnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    privateint property;
    publicint Property
    {
        get { return property; }
        set
        {
            if (value != property)
            {
                property = value;
                OnPropertyChanged();
            }
        }
    }
}

C # 8 can additionally introduce support for a similar attribute CallerArgumentExpression, which allows you to set the target argument to the string representation of the expression that is passed as the value for another argument in the same method call:


publicValidate(int[] array, [CallerArgumentExpression("array")] string arrayExpression = null)
{
    if (array == null)
    {
        thrownew ArgumentNullException(nameof(array), $"{arrayExpression} was null.");
    }
    if (array.Length == 0)
    {
        thrownew ArgumentException($"{arrayExpression} was empty.", nameof(array));
    }
}

Just like the attributes of the caller information, the compiler will set the value of the variable on the caller in the compiled code.


Target-typed new Expression


When declaring local variables, the var keyword is already used to prevent repetitions of (potentially long) type names in the code:


Dictionary<string, string> dictionary = new Dictionary<string, string>(); // без varvar dictionary = new Dictionary<string, string>(); // с var

A similar approach cannot be used for class members (fields), since they require that the type be explicitly defined:


classDictionaryWrapper
{
    private Dictionary<string, string> dictionary = new Dictionary<string, string>();
    // ...
}

The planned expression in C # 8, typed by the target type, new, will give an alternative abbreviated syntax for such cases:


classDictionaryWrapper
{
    private Dictionary<string, string> dictionary = new();
    // ...
}

This syntax, of course, will not be limited to such a context. It can be used in all cases where the type can be inferred by the compiler.


Cry soul translator

Да сделайте уже аналог Type/typedef, который применим в гораздо большем количестве сценариев и забудьте про все эти мелкие синтаксические сахаринки, которые новички будут учить еще долго! А такой вот инициализатор не подойдет даже для случая, когда поле/переменная будут иметь интерфейсный тип.


Ordering of ref and partial Modifiers on Type Declarations


C # currently requires the partial keyword to be placed immediately before the keywords of a struct or class.


When introducing the ref keyword for structures located on the stack in C # 7.2, restrictions on the partial word remained in force, which leads to the fact that the ref word should be placed immediately before the struct keyword, if there is no partial before it and just before the partial, if the last are present.


Therefore, only two variants of the syntax are valid:


publicrefstruct NonPartialStruct { }
publicrefpartialstruct PartialStruct { }

In C # 8, these restrictions are planned to be relaxed so that the following option is also valid:


publicpartialrefstruct PartialStruct { }

Conclusion


In C # 8, many new features are planned. This review does not cover all of them.
Probably, we are still quite far from the final release of C # 8.0, because its date has not yet been announced. Before that, we can fully expect that plans will change, not all opportunities will be included in the release. And even those that will be included in the final release can be changed syntactically and even semantically.


Of course, other new features that are not described in the article can be added to the language. Therefore, consider the information from this article only as an interested look into the potential future of the language.


We also recommend reading article C # 7.1, 7.2 and 7.3 - New Features (Updated) .


Translator's Note

Как переводчик, позволю добавить своё впечатление о развитии C#. В свое время уже писал об этом в статье C# — есть ли что-то лишнее? За прошедшее время, произошли некоторые изменения в ощущениях и понимании:


  1. Насчет var попустило. Его использование не сильно мешает мне читать чужой код. Только в редких случаях. Значит, я слишком беспокоился.
  2. Язык Go легко учится. Это позволяет легко войти. Но при этом приходится писать достаточно много бойлерплейт-кода. Особенно раздражает это в случаях, когда знаешь, что это легко обойти (теми же дженериками) на других языках.
  3. Язык C# очень сложный и становится все сложнее. Поскольку я с ним почти с самого зарождения (еще с версии .Net Framework 1.1), то я нормально успеваю впитать новшества (разве что «человекопонятный» синтаксис LINQ так и не зашел – возможно потому, что знаю SQL, под который он неудачно пытался мимикрировать). При этом наблюдаю, как годами учат язык новички.
  4. Так и не вводят какого-то аналога typedef, который бы снял необходимость во многих фичах сокращенного определения сложных контейнерных типов, позволил бы вводить алиасы для типов, использовать их в определениях членов класса (var работает только для локальных элементов). Вместо этого делают сокращенный new(), который применим в узком диапазоне сценариев (замечание про инициализацию переменной интерфейсного типа смотри в спойлере выше).

Языки программирования различаются по сложности, но в тех пределах, в которых их может понять человек. GO и C# находятся на разных полюсах этой сложности. От примитивизма до приближения к границе того, что можно выучить новичку в разумные сроки. В перспективе это грозит сообществу C# тем, что в него будет малый приток свежих сил – это тревожная тенденция с моей точки зрения.


Как это решить? За каждым языком тянется хвост обеспечения совместимости. Именно он не дает возможности удалять уже введенные фичи. Но у C# есть хорошая база – Common Language Infrastructure (CLI). CLI дает возможность написать новый язык с чистого листа, оставив при этом возможность работы со старым кодом, который еще долго может оставаться в рабочем состоянии. В качестве ближайшего аналога можно рассмотреть ситуацию с Java/Kotlin. Возможно, Майкрософт пойдет таким путем?


Также для постепенного выпиливания устаревших возможностей можно использовать указание версии языка (упомянутое в разделе о Nullable Reference Types). Такой подход был бы более эволюционным, но и очень продолжительным.


Also popular now: