[C#, ООП, Программирование] Волшебные методы в C# (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
10-Сен-2020 11:31

Привет, Хабр!
Сегодня мы предлагаем вам перевод статьи, которую можно отнести к жанру «imho», и которая наверняка заинтересует ценителей C#. Автор рассказывает о самых интересных и полезных, с его точки зрения, методах C# в версии 3 и выше.

В языке C# существует ряд особых сигнатур методов, поддерживаемых на уровне самого языка. Методы с такими сигнатурами приспособлены к использованию особого синтаксиса, обладающего рядом достоинств. Например, эти методы помогают упрощать код, либо позволяют создавать предметно-ориентированные языки, чтобы гораздо четче выразить проблему, специфичную для нашей предметной области. Я в разных контекстах встречал такие методы, поэтому решил написать целую статью и резюмировать все, что мне удалось нарыть по этой теме.
Синтаксис инициализации коллекций
Инициализатор коллекций – довольно старая фича, существует в языке с версии 3 (выпущенной в конце 2007 года). Просто напомню, Collection initializer позволяет заранее заполнять списки, поскольку предоставляет элементы, которые будут находиться в операторе блока:
var list = new List<int> { 1, 2, 3};

Этот код преобразуется в следующий список операторов:
var list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);

Collection initializer характерен не только для таких типов, как массивы и коллекции из BCL, а может использоваться с любым типом, удовлетворяющим следующим условиям:
  • Реализует интерфейс IEnumerable
  • Объявляет метод с сигнатурой void Add(T item)

public class CustomList<T>: IEnumerable
{
    public IEnumerator GetEnumerator() => throw new NotImplementedException();
    public void Add(T item) => throw new NotImplementedException();
}

Можно добавить поддержку Collection initializer к существующим типам, определив метод Add как метод расширения:
public static class ExistingTypeExtensions
{
    public static void Add<T>(ExistingType @this, T item) => throw new NotImplementedException();
}

Этот синтаксис также может использоваться для вставки элементов в поле коллекции, когда в блоке инициализации нет доступного сеттера:
class CustomType
{
    public List<string> CollectionField { get; private set; }  = new List<string>();
}
class Program
{
    static void Main(string[] args)
    {
        var obj = new CustomType
        {
            CollectionField =
            {
                "item1",
                "item2"
            }
        };
    }
}

Метод Add может иметь более одного параметра:
public class CustomList<T>: IEnumerable
{
    public IEnumerator GetEnumerator() => throw new NotImplementedException();
    public void Add(T item, string extraParam1, int extraParam1) => throw new NotImplementedException();
}

Для использования такой перегрузки внутри блока инициализации необходимо заключить все параметры в дополнительную пару фигурных скобок:
var obj = new CustomType
{
    CollectionField =
    {
        {"item1", "extraParamVal1", 2 },
        {"item2", "extraParamVal2", 3 }
    }
};

Collection initializer довольно часто применяется для инициализации коллекции с хорошо известным количеством элементов, но его можно задействовать и для задания коллекции с динамическим количеством элементов. В обоих случаях синтаксис будет идентичен:
var obj = new CustomType
{
    CollectionField =
    {
       existingItems
    }
};

Это можно делать с типами, удовлетворяющими следующим условиям:
  • Реализует интерфейс IEnumerable
  • Объявляет метод с сигнатурой void Add(IEnumerable<T> items)

public class CustomList<T>: IEnumerable
{
    public IEnumerator GetEnumerator() => throw new NotImplementedException();
    public void Add(IEnumerable<T> items) => throw new NotImplementedException();
}

К сожалению, массив и коллекции из BCL не реализуют метод void Add(IEnumerable<T> items), но это можно легко изменить, определив метод расширения для уже существующих типов коллекций:
public static class ListExtensions
{
    public static void Add<T>(this List<T> @this, IEnumerable<T> items) => @this.AddRange(items);
}

Благодаря такому методу расширения, теперь можно писать код, имеющий следующий вид:
var obj = new CustomType
{
    CollectionField =
    {
        existingItems.Where(x => /*Filter items*/) .Select(x => /*Map items*/)
    }
};

или даже собрать результирующую коллекцию, смешав отдельные элементы и результаты, полученные от множественных enumerables:
var obj = new CustomType
{
    CollectionField =
    {
        individualElement1,
        individualElement2,
        list1.Where(x => /*Filter items*/) .Select(x => /*Map items*/),
        list2.Where(x => /*Filter items*/) .Select(x => /*Map items*/)
    }
};

Без такого синтаксиса было бы очень сложно достичь подобного результата внутри блока инициализации.
Я открыл эту языковую фичу случайно, работая с отображениями для типов с полями коллекций, сгенерированными из контрактов protobuf. Если вы не знакомы с protobuf, но вам доводилось использовать grpctools для генерации дотнетовских типов из файлов proto, то обращу внимание, что все типы коллекций генерируются следующим образом:
[DebuggerNonUserCode]
public RepeatableField<ItemType> SomeCollectionField
{
    get
    {
        return this.someCollectionField_;
    }
}

Как видите, поля коллекций в сгенерированном коде не имеют сеттера, но нет худа без добра: RepeatableField реализует void Add(IEnumerable items), что позволяет инициализировать их в блоке инициализации:
/// <summary>
/// Вносит все указанные значения в эту коллекцию. Данный метод нужен, чтобы
/// обеспечивать создание повторяющихся полей из запросов внутри инициализаторов коллекций.
/// В коде инициализатора, не относящемся к коллекциям, попробуйте использовать эквивалентный <see cref="AddRange"/>
/// метод для ясности.
/// </summary>
/// <param name="values">Значения, которую требуется добавить в эту коллекцию.</param>
public void Add(IEnumerable<T> values)
{
    AddRange(values);
}

Синтаксис инициализации словарей
Среди крутых фич, появившихся в C# 6, были инициализаторы индекса, упростившие синтаксис инициализации словарей. Благодаря им, можно писать init-код словарей в гораздо более удобочитаемом виде:
var errorCodes = new Dictionary<int, string>
{
    [404] = "Page not Found",
    [302] = "Page moved, but left a forwarding address.",
    [500] = "The web server can't come out to play today."
};

Этот код преобразуется в:
var errorCodes = new Dictionary<int, string>();
errorCodes[404] = "Page not Found";
errorCodes[302] = "Page moved, but left a forwarding address.";
errorCodes[500] = "The web server can't come out to play today.";

Не так много, но в результате определенно становится гораздо удобнее писать и читать код.
Самая классная черта Index initializer в том, что они применимы не только с классом Dictionary<>, но и с любым типом, определяющим indexer:
class HttpHeaders
{
    public string this[string key]
    {
        get => throw new NotImplementedException();
        set => throw new NotImplementedException();
    }
}
class Program
{
    static void Main(string[] args)
    {
        var headers = new HttpHeaders
        {
            ["access-control-allow-origin"] = "*",
            ["cache-control"] = "max-age=315360000, public, immutable"
        };
    }
}

Деконструкторы
В C# 7.0 наряду с кортежами был введен механизм деконструкции. Деконструкторы позволяют «декомпозировать» кортеж в набор отдельных переменных, вот так:
var point = (5, 7);
// декомпозиция кортежа в отдельные переменные
var (x, y) = point;
что эквивалентно:
ValueTuple<int, int> point = new ValueTuple<int, int>(1, 4);
int x = point.Item1;
int y = point.Item2;

Такой синтаксис также позволяет попеременно использовать значения двух переменных без необходимости явно объявлять третью переменную:
int x = 5, y = 7;
//переключение
(x, y) = (y,x);

… или более лаконично инициализировать член:
class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y)  => (X, Y) = (x, y);
}

Деконструкторы могут использоваться не только с кортежами, но и с собственными типами. Чтобы была возможна деконструкция собственного типа, он должен реализовывать метод, подчиняющийся следующим правилам:
  • Называется Deconstruct
  • Возвращает void
  • Каждый из его параметров должен определяться с модификатором out

Для нашего типа Point мы можем определить деконструктор следующим образом:
class Point
{
    public int X {get;}
    public int Y {get;}
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

а пример его использования может выглядеть так:
var point = new Point(2,4);
var (x, y)= point;

что под капотом выглядит так:
int x;
int y;
new Point(2, 4).Deconstruct(out x, out y);

Деконструкторы можно добавлять к типам, объявленным вне исходного кода, определяя их как метод расширения:
public static class PointExtensions
{
     public static void Deconstruct(this Point @this, out int x, out int y) => (x, y) = (@this.X, @this.Y);
}

Среди наиболее полезных деконструкторов отметим предназначенный для KeyValuePair<TKey,TValue>, обеспечивающий легкий доступ к ключу и значению при переборе словаря:
foreach(var (key, value) in new Dictionary<int, string> { [1] = "val1", [2] = "val2" })
{
    // делаем что-нибудь
}

KeyValuePair<TKey,TValue>.Deconstruct(TKey, TValue) доступен лишь начиная с netstandard2.1. В предыдущих версиях netstandard его было необходимо добавлять вручную, это делалось при помощи метода расширения.
Собственные ожидаемые типы
Версия C# 5 (выпущенная вместе с Visual Studio 2012) ввела в дело механизм async/await, поистине переломивший ситуацию в области асинхронного программирования. Ранее обработка вызова асинхронных методов весьма часто требовала писать весьма путаный код, особенно в случаях, когда асинхронный вызов был не один:
void DoSomething()
{
    DoSomethingAsync().ContinueWith((task1) => {
        if (task1.IsCompletedSuccessfully)
        {
            DoSomethingElse1Async(task1.Result).ContinueWith((task2) => {
                if (task2.IsCompletedSuccessfully)
                {
                    DoSomethingElse2Async(task2.Result).ContinueWith((task3) => {
                        //TODO: Do something
                    });
                }
            });
        }
    });
}
private Task<int> DoSomethingAsync() => throw new NotImplementedException();
private Task<int> DoSomethingElse1Async(int i) => throw new NotImplementedException();
private Task<int> DoSomethingElse2Async(int i) => throw new NotImplementedException();

Синтаксис async/await позволяет записать то же самое гораздо чище:
async Task DoSomething()
{
    var res1 = await DoSomethingAsync();
    var res2 = await DoSomethingElse1Async(res1);
    await DoSomethingElse2Async(res2);
}

Возможно, вы удивитесь, но ключевое слово await не зарезервировано для работы только лишь с типом Task. Оно может использоваться с любым типом, который содержит метод GetAwaiter и возвращает тип, удовлетворяющий следующему требованию:
  • Реализует интерфейс System.Runtime.CompilerServices.INotifyCompletion с методом void OnCompleted(Action continuation).
  • Содержит булево свойство IsCompleted.
  • Содержит метод GetResult, не имеющий параметров

Чтобы добавить поддержку ключевого слова await к собственному типу, необходимо определить метод GetAwaiter, возвращающий экземпляр TaskAwaiter<TResult> или собственный тип, удовлетворяющий вышеуказанным условиям.
class CustomAwaitable
{
    public CustomAwaiter GetAwaiter() => throw new NotImplementedException();
}
class CustomAwaiter: INotifyCompletion
{
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public bool IsCompleted => => throw new NotImplementedException();
    public void GetResult() => throw new NotImplementedException();
}

Возможно, вам интересно, существует ли сценарий использования синтаксиса await с собственным ожидаемым типом. Если я угадал, то настоятельно рекомендую вам статью Стивена Тауба под названием “Await Anything”, в которой вас ждет масса интересных примеров.
Паттерн выражений запросов
Наилучшим изобретением в рамках C# 3.0 определенно был Language-Integrated Query, также известный как LINQ, обеспечивающий операции над коллекциями при помощи SQL-подобного синтаксиса. Существует две вариации LINQ: SQL-подобный синтаксис и синтаксис методов расширений. Я предпочитаю второй вариант, так как он кажется мне более удобочитаемым; наверное, я просто привык к нему. Интересный факт о SQL-подобном синтаксисе: оказывается, он преобразуется в синтаксис методов расширений при компиляции, так как это фича C#, а не CLR. LINQ был изобретен, прежде всего, для работы с типами IEnumerable, IEnumerable<T> и IQueryable<T>, но применим не только с ними, а с любым типом, удовлетворяющим требованиям паттерна выражений запросов. Вот полный список сигнатур методов, используемых LINQ:
class C
{
    public C<T> Cast<T>();
}
class C<T> : C
{
    public C<T> Where(Func<T,bool> predicate);
    public C<U> Select<U>(Func<T,U> selector);
    public C<V> SelectMany<U,V>(Func<T,C<U>> selector, Func<T,U,V> resultSelector);
    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);
    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);
    public O<T> OrderBy<K>(Func<T,K> keySelector);
    public O<T> OrderByDescending<K>(Func<T,K> keySelector);
    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);
    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector);
}
class O<T> : C<T>
{
    public O<T> ThenBy<K>(Func<T,K> keySelector);
    public O<T> ThenByDescending<K>(Func<T,K> keySelector);
}
class G<K,T> : C<T>
{
    public K Key { get; }
}

Естественно, не требуется использовать всех этих методов, чтобы использовать синтаксис LINQ с нашим собственным типом. Отличное объяснение того, как работать с этим синтаксисом, приведено в статье Understand monads with LINQ.
Итоги
В этой статье я не пытался склонить вас к злоупотреблению перечисленными здесь синтаксическими трюками, а, скорее, хотел разъяснить их. С другой стороны, не следует полностью их избегать. Они были изобретены, так как в них имелась нужда, и иногда они позволяют сделать код гораздо чище. Если вы опасаетесь, что получится код, устройство которого покажется неочевидным вашим коллегам, просто посоветуйте им почитать эту статью;).
===========
Источник:
habr.com
===========

===========
Автор оригинала: CEZARY PIĄTEK
===========
Похожие новости: Теги для поиска: #_c#, #_oop (ООП), #_programmirovanie (Программирование), #_c#, #_.net, #_oop (ООП), #_programmirovanie (программирование), [url=https://torrents-local.xyz/search.php?nm=%23_blog_kompanii_izdatelskij_dom_«piter»&to=0&allw=0&o=1&s=0&f%5B%5D=820&f%5B%5D=959&f%5B%5D=958&f%5B%5D=872&f%5B%5D=967&f%5B%5D=954&f%5B%5D=885&f%5B%5D=882&f%5B%5D=863&f%5B%5D=881&f%5B%5D=860&f%5B%5D=884&f%5B%5D=865&f%5B%5D=873&f%5B%5D=861&f%5B%5D=864&f%5B%5D=883&f%5B%5D=957&f%5B%5D=859&f%5B%5D=966&f%5B%5D=956&f%5B%5D=955]#_blog_kompanii_izdatelskij_dom_«piter» (
Блог компании Издательский дом «Питер»
)[/url], #_c#, #_oop (
ООП
)
, #_programmirovanie (
Программирование
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 18:58
Часовой пояс: UTC + 5