C# 7.0 Новые функции

C# 7 – Что нового

Совсем недавно была выпущена новая версия языка программирования C#. В этой статье я расскажу об основных новшествах в новой версии C# 7.

C# 7 – Основное направление улучшений

Во всех предыдущих выпусках C# (возможно за исключение C# 6), все улучшения были сосредоточенны вокруг некоторой основной темы:
C# 2.0 – вводятся Generic типы.
C# 3.0 – добавляется LINQ при помощи внедрения методов расширений (extension methods), добавляются лямбда выражения, анонимные типы и другие улучшения, необходимые для удобной работы с LINQ.
C# 4.0 – все улучшения сосредоточенны на улучшении совместимости (interoperability) при помощи динамических не строго типизированных переменных (dynamic),
C# 5.0 – упрощается асинхронное программирование при помощи добавления ключевых слов async/await
C# 6.0 – полностью переписывается компилятор, добавляются несколько улучшений и новых фич.

И новая версия языка C# 7.0 не исключение. Разработчики компилятора сосредоточились на трех основных направлениях:

Работа с данными — популярность веб-служб приводит к изменению способа моделирования данных. Вместо того, чтобы разрабатывать модели данных как часть приложения, их определение становится частью контрактов веб-сервисов. Хотя это очень удобно в функциональных языках, это может принести дополнительную сложность в объектно-ориентированную разработку приложений. Несколько новых функций C# 7.0 нацелены на упрощение работы с контрактами внешних данных.

Улучшенная производительность — рост доли приложений для мобильных устройств оказывает свое влияние, и теперь компилятор должен генерировать высокопроизводительный код, который может быстро и надежно исполняться на мобильных платформах. В C # 7.0 представлены функции, которые позволяют оптимизировать производительность, которые ранее были недоступны для платформы .NET.

Упрощение кода – новый компилятор вносит несколько дополнительных небольших изменений, основанных на разработках для C# 6.0, которые позволяют еще больше упростить написание качественного и красивого код.

C # 7.0 не поддерживается в Visual Studio 2015 (и соответственно более ранними версиями).
Чтобы начать использовать новые функции, вам необходимо загрузить и установить Visual Studio 2017.

А теперь давайте рассмотрим подробнее все эти новые функции.

C# 7.0 — Работа с данными

Объектно-ориентированные языки программирования, такие как C #, отлично подходят для случаев, когда требуется работа с расширяемым набором типов данных при помощи предопределенного набора операций. Обычно они моделируются с помощью интерфейса (или базового класса), определяющего доступные операции и потенциально растущий набор классов, представляющих типы данных. Реализуя интерфейс, классы содержат реализацию всех ожидаемых операций, которые могут быть произведены над объектом.

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

interface IEnemy
{
    int Health { get; set; }
}

interface IWeapon
{
    int Damage { get; set; }

    void Attack(IEnemy enemy);
    void Repair();
}

class Sword : IWeapon
{
    public int Damage { get; set; }
    public int Durability { get; set; }

    public void Attack(IEnemy enemy)
    {
        if (Durability > 0)
        {
            enemy.Health -= Damage;
            Durability--;
        }
     }

     public void Repair()
     {
         Durability += 100;
     }
}

class Bow : IWeapon
{
    public int Damage { get; set; }
    public int Arrows { get; set; }

    public void Attack(IEnemy enemy)
    {
        if (Arrows > 0)
        {
            enemy.Health -= Damage;
            Arrows--;
        }
    }

    public void Repair()
    { }
}

В функциональном программировании типы данных не содержат операции. Вместо этого каждая функция реализует единую операцию для всех типов данных. Это значительно упрощает добавление новой операции (просто определить и внедрить новую функцию), но усложняет добавление нового типа данных (необходимо модифицировать все существующие функции). Программирование в таком стиле уже было возможно в C# 6, хотя код получается намного боле громоздким, чем могло бы быть.

interface IEnemy
{
    int Health { get; set; }
}
 
interface IWeapon
{
    int Damage { get; set; }
}
 
class Sword : IWeapon
{
    public int Damage { get; set; }
    public int Durability { get; set; }
}
 
class Bow : IWeapon
{
    public int Damage { get; set; }
    public int Arrows { get; set; }
}
 
static class WeaponOperations
{
    static void Attack(this IWeapon weapon, IEnemy enemy)
    {
        if (weapon is Sword)
        {
            var sword = weapon as Sword;
            if (sword.Durability > 0)
            {
                enemy.Health -= sword.Damage;
                sword.Durability--;
            }
        }
        else if (weapon is Bow)
        {
            var bow = weapon as Bow;
            if (bow.Arrows > 0)
            {
                enemy.Health -= bow.Damage;
                bow.Arrows--;
            }
        }
    }
 
    static void Repair(this IWeapon weapon)
    {
        if (weapon is Sword)
        {
            var sword = weapon as Sword;
            sword.Durability += 100;
        }
    }
}

Pattern matching – это функция, которая должна помочь упростить приведенный выше код. Ниже пример, как это может выглядеть если применить его к методу Attack():

static void Attack(this IWeapon weapon, IEnemy enemy)
{
    if (weapon is Sword sword)
    {
        if (sword.Durability > 0)
        {
            enemy.Health -= sword.Damage;
            sword.Durability--;
        }
    }
    else 
    if (weapon is Bow bow)
    {
        if (bow.Arrows > 0)
        {
            enemy.Health -= bow.Damage;
            bow.Arrows--;
        }
    }
}

Вместо того, чтобы проверять тип оружия и назначать его правильной типизированной переменной в двух отдельных операциях, теперь оператор is также позволяет нам объявить новую переменную и присвоить ей значение типа.

С аналогичным результатом можно использовать оператор case switch вместо if. Это может сделать код еще более понятным, особенно когда есть много разных ветвей:

switch (weapon)
{
    case Sword sword when sword.Durability > 0:
        enemy.Health -= sword.Damage;
        sword.Durability--;
        break;
    case Bow bow when bow.Arrows > 0:
        enemy.Health -= bow.Damage;
        bow.Arrows--;
        break;
}

Обратите внимание, как оператор case выполняет как присвоение типа, так и дополнительную условную проверку, делая код намного более компактным и понятным.

Кортежи / Tuples

Существует еще одна новая фича в C# 7.0, которая должна способствовать созданию более компактного кода: кортежи (tuples). Это новая, более легковесная альтернатива анонимным классам. Их основное использование, вероятно, будет в функциях, возвращающих несколько значений, в качестве альтернативы аргументам с модификатором out. Пример:

public static (int weight, int count) Stocktake(IEnumerable<IWeapon> weapons)
{
	var w = 0;
	var c = 0;
	foreach (var weapon in weapons)
	{
		w += weapon.Weight;
		c++;
	}
	return (w, c);
}

ВАЖНО: Чтобы использовать кортежи, вам необходимо установить NuGet пакет System.ValueTuple, который добавляет необходимый тип System.ValueType<> обобщенного типа.

Install NuGet пакет для поддержки System.ValueTuple

Установка NuGet пакет для поддержки System.ValueTuple

Метод выше возвращает два отдельных значения без использования аргументов с модификатором out и без необходимости объявлять новый специальный тип или использовать анонимную переменную. Синтаксис вызова метода остается прежним:

static void Main(string[] args)
{
	List<IWeapon>   inventory = new List<IWeapon>();

	Sword   sword = new Sword();
	sword.Damage = 100;
	sword.Durability = 200;
	sword.Weight = 50;

	inventory.Add(sword);
	
	var inventoryInfo = Tuples.Stocktake((IEnumerable<IWeapon>)inventory);

	Console.WriteLine($"Weapon count: {inventoryInfo.count}");
	Console.WriteLine($"Total weight: {inventoryInfo.weight}");
}

Возвращаемое значение похоже на обобщенный класс Tuple<> из .NET Framework. Однако есть два важных отличия:
1. Его члены могут иметь значащие имена, определенные в вызываемом методе, и не ограничиваются не описательными Item1, Item2 и т. д. Это делает код лучше читаемым и более понятным.
2. Это переменная типа значения, а не переменная ссылочного типа. Т.е. нет расходов на выделение памяти.
А еще, в качестве альтернативы, при вызове метода можно использовать новую функцию языка деконструкцию (deconstruction) для возвращения значений в локальные переменные:

(var inventoryWeight, var inventoryCount) = Tuples.Stocktake((IEnumerable)inventory);
Console.WriteLine($"Weapon count: {inventoryCount}");
Console.WriteLine($"Total weight: {inventoryWeight}");

В этом случае вы определяете имена для отдельных членов возвращаемого значения. Этот синтаксис позволит игнорировать возвращаемые значения, которые вас не интересуют, используя отбрасывания (обратите внимание на использование нижнего подчеркивания _ вместо объявлений переменных в приведенном ниже коде):

(var inventoryWeight2, _) = Tuples.Stocktake((IEnumerable)inventory);
Console.WriteLine($"Total weight: {inventoryWeight2}");

Деконструкция (deconstruction) может применяться не только при работе с кортежами. Любой тип может использовать деконструкцию, если есть объявленный метод деконструкции для этого типа. Пример:

public static void Deconstruct(this Sword sword, out int damage, out int durability)
{
    damage = sword.Damage;
    durability = sword.Durability;
}

Такой метод расширения делает следующий код вполне валидным:

(var damage, var durability) = sword;
Console.WriteLine($"Sword damage rating: {damage}");
Console.WriteLine($"Sword durability : {durability}");

Главное условие – тип должен содержать метод с именем Deconstruct, или должен быть объявлен метод расширения с именем Deconstruct.

Улучшенная производительность в C# 7.0

Улучшения производительности в C # 7.0 сосредоточены на сокращении копирования данных между ячейками памяти.

Локальные функции

Локальные функции (Local functions) позволяют объявлять вспомогательные функции, вложенные в другие функции. Это не только уменьшает их объем, но также позволяет использовать переменные, объявленные в их охватывающей области, без выделения дополнительной памяти в куче или стеке:

static void ReduceMagicalEffects(this IWeapon weapon, int timePassed)
{
	double CalculateDecayRate() => 0.1;

	double decayRate = CalculateDecayRate();

	double GetRemainingEffect(double currentEffect) => 
		currentEffect * Math.Pow(decayRate, timePassed);

	weapon.FireEffect       = GetRemainingEffect(weapon.FireEffect);
	weapon.IceEffect        = GetRemainingEffect(weapon.IceEffect);
	weapon.LightningEffect  = GetRemainingEffect(weapon.LightningEffect);
}

Локальные переменные по ссылке

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

public void LocalVariableByReference()
{
    var terrain = Terrain.Get();
 
    ref TerrainType terrainType = ref terrain.GetAt(4, 2);
    Assert.AreEqual(TerrainType.Grass, terrainType);
 
    // Изменить значение enum в исходном местоположении
    terrain.BurnAt(4, 2);
    
    // локальная переменная также была затронута и изменила свое значение
    Assert.AreEqual(TerrainType.Dirt, terrainType);
}

В приведенном выше примере terrainType является локальной переменной по ссылке, а GetAt — функцией, возвращающей значение по ссылке:

public ref TerrainType GetAt(int x, int y) =&gt; ref terrain[x, y];

Производительность для асинхронных методов

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

В настоящее время все асинхронные методы должны возвращать Task , который является ссылочным типом. Теперь доступен тип значения: ValueTask.

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

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

Упрощения в написании кода в C# 7.0

Теперь немного поговорим о синтаксическом сахаре.

В C# 6.0 появилась поддержка методов — выражений и свойств только для чтения. Теперь свойства чтения / записи, конструкторы и финализаторы также поддерживают эти фичи:

class Grunt : IEnemy
{
    // Используем запись тела конструктора в виде выражения
    public Grunt(int health) => _health = health >= 0 ? health : throw new ArgumentOutOfRangeException();

         // Тоже самое для финализатора – тело метода записано в виде выражения
    ~Grunt() => Debug.WriteLine("Finalizer called");
 
         // Тело для методов свойства также можно записывать в виде выражения
    private int _health;
    public int Health
    {
        get => _health = 0;
        set => _health = value >= 0 ? value : 0;
    }
}

В этом классе используется еще одна новая интересная функция: throw expression. Конструктор выдает исключение для отрицательных значений здоровья. Раньше это было возможно только из утверждения (statement), теперь выражения также поддерживают его.
Чтобы передать переменную в выходной аргумент функции до C # 7.0, вам нужно было объявить ее заранее:

ISpell spell;
if (learnedSpells.TryGetValue(spellName, out spell))
{
   spell.Cast(enemy);
}

Теперь вы можете объявить переменную out непосредственно в списке аргументов функции:

if (learnedSpells.TryGetValue(spellName, out var spell))
{
    spell.Cast(enemy);
}

И последнее улучшение, о котором следует упомянуть, — это улучшение числовых литералов:

const int MAX_PLAYER_LEVEL = 0b0010_1010;

В C# 7.0 добавлена поддержка бинарных литералов. Чтобы сделать их более читаемыми, можно использовать в качестве разделителя цифр можно использовать нижнее подчеркивание. А еще подчеркивание можно использовать в десятичных и шестнадцатеричных литералах.
Вот такие новшества принесла нам новая седьмая версия языка C#.

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

Ваш e-mail не будет опубликован.