Новые возможности в C# 7.3 – Управляемый код

C# logoОдновременно с очередным обновлением Visual Studio 2017 (версия 15.7) стала доступна новая версия C# под номером 7.3. Посмотрим какие новые возможности для написания управляемого кода появились в ней.

Оптимизация выбора перегруженных методов

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

  • Если вызов метода осуществляется при помощи экземпляра объекта или результат возвращается в экземпляр, то статические методы исключаются из списка кандидатов.
    • и наоборот, исключаются методы экземпляра при отсутствии экземпляра объекта для вызова метода или в качестве получателя результата.
  • При отсутствии получателя возвращаемого значения
    • при вызове в статическом контексте включаются только статические методы,
    • в противном случае и статические и методы экземпляра.
  • Если существует двусмысленность выбора из-за типа, который принимает возвращаемое значение, то включаются и статические методы и методы экземпляра.
    • при вызове метода в статическом контексте в список включаются только статические методы.
    • в противном случае включаются и статические и методы экземпляра.
  • Generic методы, у которых условия вызова не удовлетворяют наложенным в них ограничениям, исключаются из списка кандидатов.
  • При преобразовании группы методов (например something.Where(MyOverloadedMethod)), из списка кандидатов удаляются методы, возвращаемый тип которых не совпадает с возвращаемым типом делегата.

Сравнение кортежей

C# 7.3 добавляет возможность сравнивать кортежи с помощью операций == и !=.

Пусть t1 и t2 это два экземпляра кортежа. Тогда:

  • t1 == t2 это аналог записи
    t1.Item1 == t2.Item1 && t1.Item2 == t2.Item2 && …
  • t1 != t2 равнозначна
    t1.Item1 != t2.Item1 ||  t1.Item2 != t2.Item2 || …

Если тип кортежи обернуты в Nullable<T>, то добавляется сравнение свойств HasValue. Т.е. тогда t1 == t2 будет эквивалентно записи

t1.HasValue == t2.HasValue ? (t1.HasValue ? … : true) : false

Сравнение двух объектов может вернуть тип, отличный от bool (например если были определены пользовательские operator == и operator !=). В этом случае результат будет приведен к типу bool , а если это не возможно, то будет попытка вызвать operator bool для результата.

Обратите внимание, что сравнение идет с помощью свойств Item{n}. Определенные пользователем имена свойств роли не играют. Поэтому можно сравнивать кортежи с различными пользовательскими именами. Также в аналогичных позициях свойства двух кортежей могут быть различного типа, но для них должна быть определена операция сравнения. Например:

(float a, int b) t1 = (1.0f, 42);
(int c, double d) t2 = (1, 42);

Console.WriteLine(t1 == t2); // выведет "True"

Если сравнить значения не возможно, то это приведет к ошибке на этапе компиляции:

(float a, int b) t1 = (1.0f, 42);
(int c, string d) t2 = (1, "42");

Console.WriteLine(t1 == t2); // Ошибка компиляции: не определено == для string и int

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

Еще один интересный момент это наличие экземпляров объектов в составе кортежа. Их сравнение в аналогичных позициях Item{n} проходит по правилам:

  • Если оба экземпляра равны null, то их сравнение считается успешным, даже если типы самих объектов различные.
  • Если экземпляры не null, одного типа и определен operator ==, то для сравнения вызывается operator ==.
  • Во всех остальных случаях происходит сравнение по ссылке.

Поддержка Enum, Delegage и unmanaged в качестве ограничений generic параметра

Список ограничителей generic параметров пополнился 3 новыми значениями: Enum, Delegate и unmanaged. Первые два из них разрешают использование в качестве generic только Enum и Delegate соответственно. Например:

public interface IProcessor
{
    void Process<T>(T value)
        where T : Enum;
}

public enum UserRole { Admin, User, Guest }


var role = UserRole.Admin;
processor.Process(role);
// или
processor.Process(UserRole.User);

processor.Process(new object ()); // ошибка компиляции

Обратите внимание, что необходимо указывать Enum и Delegate как имена типов, а не ключевые слова. Запись "where T : enum" выдаст ошибку на этапе компиляции.

Третьи вариант “unmanaged” накладывает следующие ограничения на тип:

  • Это структура (struct), включая системные enum, int и т.д.
  • Все ее поля должны соответствовать одному из следующих правил:
    • Имеют один из указанных типов:  sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, IntPtr или UIntPtr.
    • Являются перечислением (enum)
    • Являются указателем
    • Являются структурой, которая также соответствует ограничению unmanaged

Ограничение "unmanaged" не является ключевым словом и не может быть использовано вместе с struct, class или new().

Переменные выражений в инициализаторах и запросах

В C# 7.3 убраны ограничения на использование out переменных и переменных в шаблонах объявлений (например, case int t) в:

  • Инициализаторах конструкторов. Переменная будет доступна в теле конструктора.
  • Инициализаторах свойств и полей. Переменная будет доступна в самом выражении инициализатора.
  • В LINQ запросах, если выражение транслируется в тело лямбды. Переменная будет доступна в теле выражения.
public class RootNode : NodeBase
{
    private string _sVal = "3";

    public int Value => int.TryParse(this._sVal, out int iVal) ? iVal : 0;

    public RootNode(string sValue)
        : base(int.TryParse(sValue, out int value))
    {
        var source = new string[] { "1", "b", "3" };
        var query = from s in source
                    where int.TryParse(s, out var iValue) && iValue != value
                    select s;
    }
}

Конструкция [field] для автоматически реализованных свойств

Теперь для полей, сгенерированных компилятором для автоматически реализованных свойств, можно указать атрибуты с помощью конструкции [field <Attribute>]. Например:

[Serializable]
public class User {

    …

    [field: NonSerialized]
    public string TemoraryComment { get; set; }
}

Данный код будет преобразован в следующий:

[Serializable]
public class User {
    [NonSerialized]
    private string TemoraryComment_backingField;

    …
    
    public string TemoraryComment {
        get { return TemoraryComment_backingField; }
        set { TemoraryComment_backingField = value; }
    }
}

Добавить комментарий