Магические сигнатуры методов в C#

Original author: CEZARY PIĄTEK
  • Translation

Представляю вашему вниманию перевод статьи The Magical Methods in C# автора CEZARY PIĄTEK.


Есть определенный набор сигнатур методов в C#, имеющих поддержку на уровне языка. Методы с такими сигнатурами позволяют использовать специальный синтаксис со всеми его преимуществами. Например, с их помощью можно упростить наш код или создать DSL для того, чтобы выразить решение проблемы более красивым образом. Я встречаюсь с такими методами повсеместно, так что я решил написать пост и обобщить все мои находки по этой теме, а именно:


  • Синтаксис инициализации коллекций
  • Синтаксис инициализации словарей
  • Деконструкторы
  • Пользовательские awaitable типы
  • Паттерн query expression

Синтаксис инициализации коллекций


Синтаксис инициализации коллекции довольно старая фича, т. к. она существует с C# 3.0 (выпущен в конце 2007 года). Напомню, синтаксис инициализации коллекции позволяет создать список с элементами в одном блоке:


var list = new List<int> { 1, 2, 3 };

Этот код эквивалентен приведенному ниже:


var temp = new List<int>();
temp.Add(1);
temp.Add(2);
temp.Add(3);
var list = temp;

Возможность использования синтаксиса инициализации коллекции не ограничивается только классами из 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();
}

Мы можем добавить поддержку синтаксиса инициализации коллекции, определив Add как метод расширения:


public static class ExistingTypeExtensions
{
    public static void Add<T>(this 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"
            }
        };
    }
}

Синтаксис инициализации коллекции полезен при инициализации коллекции известным числом элементов. Но что если мы хотим создать коллекцию с переменным числом элементов? Для этого есть менее известный синтаксис:


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*/) }
    }
};

Или даже собрать коллекцию из смеси индивидуальных элементов и результатов нескольких перечислений (IEnumerable):


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 для генерации типов .NET из файлов proto, все поля-коллекции генерируются подобным образом:


[DebuggerNonUserCode]
public RepeatableField<ItemType> SomeCollectionField
{
    get
    {
        return this.someCollectionField_;
    }
}

Как можно заметить, поля-коллекции не имеют сеттер, но RepeatableField реализует метод void Add(IEnumerable items), так что мы по-прежнему можем инициализировать их в блоке инициализации:


/// <summary>
/// Adds all of the specified values into this collection. This method is present to
/// allow repeated fields to be constructed from queries within collection initializers.
/// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>
/// method instead for clarity.
/// </summary>
/// <param name="values">The values to add to this collection.</param>
public void Add(IEnumerable<T> values)
{
    AddRange(values);
}

Синтаксис инициализации словарей


Одна из крутых фич C# 6.0 — инициализация словаря по индексу, которая упростила синтаксис инициализации словарей. Благодаря ей мы можем писать более читаемый код:


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.";

Это немного, но это определенно упрощает написание и чтение кода.


Лучшее в инициализации по индексу — это то, что она не ограничивается классом Dictionary<T> и может быть использована с любым другим типом, определившим индексатор:


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);
// decomposing tuple into separated variables
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;
//switch
(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" })
{
    //TODO: Do something
}

KeyValuePair<TKey, TValue>.Deconstruct(TKey, TValue) доступно только с netstandard2.1. Для предыдущих версий netstandard нам нужно использовать ранее приведенный метод расширения.


Пользовательские awaitable типы


C# 5.0 (выпущен вместе с 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 с пользовательским awaitable типом?". Если это так, то я рекомендую вам прочитать статью Stephen Toub под названием "await anything", которая показывает множество интересных примеров.


Паттерн query expression


Лучшее нововведение C# 3.0 — Language-Integrated Query, также известное как LINQ, предназначенное для манипулирования коллекциями с SQL-подобным синтаксисом. LINQ имеет две вариации: SQL-подобный синтаксис и синтаксис методов расширения. Я предпочитаю второй вариант, т. к. по моему мнению он более читаем, а также потому что я привык к нему. Интересный факт о LINQ заключается в том, что SQL-подобный синтаксис во время компиляции транслируется в синтаксис методов расширения, т. к. это фича C#, а не CLR. LINQ был разработан в первую очередь для работы с типами IEnumerable, IEnumerable<T> и IQuerable<T>, но он не ограничен только ими, и мы можем использовать его с любым типом, удовлетворяющим требованиям паттерна query expression. Полный набор сигнатур методов, используемых 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 с нашим пользовательским типом. Список обязательных операторов и методов LINQ для них можно посмотреть здесь. Действительно хорошее объяснение того, как это сделать, можно найти в статье Understand monads with LINQ автора Miłosz Piechocki.


Подведение итогов


Цель этой статьи заключается вовсе не в том, чтобы убедить вас злоупотреблять этими синтаксическими трюками, а в том, чтобы сделать их более понятными. С другой стороны, их нельзя всегда избегать. Они были разработаны для того, чтобы их использовать, и иногда они могут сделать ваш код лучше. Если вы боитесь, что получившийся код будет непонятен вашим коллегам, вам нужно найти способ поделиться знаниями с ними (или хотя бы ссылкой на эту статью). Я не уверен, что это полный набор таких "магических методов", так что если вы знаете еще какие-то — пожалуйста, поделитесь в комментариях.

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 65

    +4

    Спасибо, я уже много лет использую C# в работе, но о некоторых вещах узнал впервые, особенно про инициализацию коллекции переменным числом элементов.

      +7

      Ещё есть CallerFilePathAttribute и его друзья — неявно передают в строковый параметр метода путь/строку исходного файла из которого метод вызван. Бывает полезно иногда (например вернуть строку упавшего ассерта в тестах)


      Ещё более злая магия — интерфейс (не помню название) который позволяет в рантайме решить какие «методы/свойства/итп доступны у объекта». В сочетании с dynamic keyword позволяет вызывать такие методы как будто бы они объявлены в классе на этапе компиляции. Классический пример ExpandoObject. Это конечно уже совсем злая магия перед использованием которой надо сто раз подумать.


      А ещё используя Roslyn можно компилировать и выполнять выражения на лету (как eval в js только лучше, т.к. позволяет корректно пробросить «контекст»). Тоже из серии злой магии.

        0

        Если я правильно помню — в WCF был такой класс как ProxyObject (или как-то так), который прикидывался, что он умеет во все интерфейсы. Уж не помню на какой чёрной магии он был сделан, но проверка obj is IAnyInterface всегда давала true.


        У меня коллега на нем даже свой IoC строил, в основе которого была реализация на основе этого класса и инфраструктурный интерфейс IThisable (не спрашивайте, зачем он такой был нужен, просто не спрашивайте)

          0
          Наверное RealProxy. Он работает на черной магии которая реализована неизвестным способом — реализация упирается в intrinsic RemotingServices.CreateTransparentProxy (реализация этого метода неизвестна науке и, похоже, является встроенной в язык). К тому же его не очень удобно использовать бывает.

          Поэтому из не-майкрософт кода его вроде как вытеснил Castle DynamicProxy, который хоть и сложный, но работает на понятных механизмах (Reflection.Emit). В будущем наверное появится что-то основанное на Roslyn, все-таки «компилировать на лету» сильно удобнее чем вручную эмиттить IL через Reflection как это делает Castle.
            0

            Точняк, спасибо.


            А насчет эмитта: худо-бедно что-то генерировать можно будет когда стабилизируют replace/original в пятом неткоре. Щас пока превью, работает криво, но к концу года должно выкатиться.

              0
              все-таки «компилировать на лету» сильно удобнее чем вручную эмиттить IL через Reflection

              Неа, в простейших случаях (а подобные прокси — именно что простейшие случаи) эмитить IL проще. И намного быстрее.

                0
                нуу… может вы и правы :)
        +9

        Сколько себя помню, всегда инициализировал словари через


        new Dictionary<string, int> {{"One", 1}, {"Two", 2}};

        Синтаксис с ["One"] = 1, конечно, намного лучше. Под капотом, оказывается, вызываются разные методы, и результат может быть разным: в одном случае это void Add(KeyValuePair<TKey, TValue> element), в другом это this[TKey] = value (void set_Item(TKey key, TValue value)).


        В список можно добавить еще void Dispose() для ref struct, ну и всякие [CallerMemberName].

          0

          UPD: Заметил, что написал не правильно. new Dictionary<string, int> {{"One", 1}}; вызывает метод Dictionary<string, int>.Add("One", 1), т.е. метод с двумя аргументами. Ниже было правильно замечено, что внутренние фигурные скобки как раз и нужны для случая, когда методы Add могут принимать разное количество аргументов, для группировки.

          +5

          В синтаксисе с инициализацией коллекцией, если уж есть Add с IEnumerable, то по идее дополнительные скобки не обязательны, т. к. это частный случай инициализации одним элементом. (Проверка.)

            +2

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

            +1
            ы можете спросить: "Каков возможный сценарий использования синтаксиса await с пользовательским awaitable типом?". Если это так, то я рекомендую вам прочитать статью Stephen Toub под названием "await anything", которая показывает множество интересных примеров.

            Можно пойти дальше и реализовать всю асинхронщину самостоятельно.


            Например, несколько лет назад не было .NET Core, а при запуске под Mono в Linux у приложения был запредельно низкий IOPS. Решение проблемы: переписать таски и планировщик самостоятельно, выкинув лишнее и сделав упор на минимальный оверхед. Заодно и под виндой IOPS вырос в несколько раз.

              0
              Можно пойти дальше и реализовать всю асинхронщину самостоятельно.

              Можно пойти ещё дальше и реализовать монадическую ду-нотацию

              –2
              Не понял, о чем статья. Собрали все в кучу
                +1
                Статья о том, как можно заваливать разрабов на собеседованиях. Очевидно же ж :)
                +1

                Банальный foreach забыли.


                А вот using почему-то прибит к IDisposable.

                  +3

                  Интерфейс IEnumerable говорит о том, что тип можно перечислять, однако ни к чему не обязывает.


                  Интерфейс IDisposable же говорит, что нам необходимо вызвать Dispose(), иначе возникнет утечка. Поэтому явно помечать тип интерфейсом, а не полагаться на наличие метода с определенным именем, важно для поддерживаемости.


                  Сложно представить себе ситуацию, когда к существующему классу потребуется Dispose() с помощью метода-расширения.

                    +1

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


                    Так делать не очень рекомендуется, но никакого физического запрета на невызов диспоза — нет

                      +1
                      о крайней мере все примитивы STD которые его имеют реализуют паттерн IDisposable, который очищает ресурсы в дестркуторе если диспоз забыли.


                      Это защита от утечек хендлов. На самом деле весь механизм финализации это большой костыль от того, что нельзя гарантировать вызов Dispose.

                      Вызов Dispose в конце работы с объектом обязателен, если такой объект реализует IDisposable.
                        –2

                        Это слишком категорично. DbContext в Entity Framework является IDisposable, но прекрасно работает и без единого вызова Dispose. Да и бывает так, что Dispose попросту негде звать.

                          +5
                          В таких случаях за вызов Dispose отвечает DI-контейнер.
                          +1
                          Вызов Dispose в конце работы с объектом обязателен, если такой объект реализует IDisposable.

                          Исключения есть, например, MemoryStream реализует IDisposable, но вызывать его не обязательно.

                          –1

                          А разве паттерн Disposable именно про это?
                          Там речь идет про освобождение неуправляемых ресурсов.
                          В обычной ситуации вы вызываете void Dispose(), который вызывает void Dispose(true), который очищает управляемые ресурсы (если надо) и неуправляемые.
                          Если же по какой-то причине Dispose не вызыван, а сборка мусора уже идет, то срабатывает финализатор (который мог бы быть вручную отменен через GC.SuppressFnalize(this)), который вызывает все тот же void Dispose(false), где параметр позволяет различить между вызовами из разных мест. При вызове из финализатора управляемые ресурсы очищать бессмысленно — их состояние неизвестно, а вот неуправляемые, наоборот, надо.
                          Таким образом, это скорее "Dispose вызывать нужно, но если что-то пошло не так, есть шанс корректно завершить работу с неуправляемыми ресурсами".

                            +3
                            Dispose может использоваться для реализации любой acquire-release семантики. docs.microsoft.com/en-us/dotnet/api/system.iobservable-1.subscribe?view=netcore-3.1
                            Вот тут IDisposable используется для отписки и явно не связан с неуправляемыми ресурсами.
                              +2
                              Или, например, из коробки есть регистрация callback'ов на токене отмены с возможностью отписаться через вызов Dispose:
                              CancellationTokenSource cts = new CancellationTokenSource();
                              CancellationTokenRegistration registration =
                                  cts.Token.Register(() => Console.WriteLine("Canceled"));
                              registration.Dispose(); // если закомментировать, выведется 'Canceled'
                              cts.Cancel();
                              
                              +1

                              В том числе и про это, ведь не зря в нём есть часть с деструктором.

                              0

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

                                0

                                Согласен, что не вызвать диспоз который есть — плохо


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

                                  0

                                  Dispose — это детерминированное освобождение ресурсов, деструктор — недетерминированное. Разумеется, недетерминированное лучше, чем никакого вообще, однако этого не всегда достаточно. Например, если открыть файл на чтение и не закрыть handle, файл останется в "подвешенном" состоянии до неопределенного времени.

                            +3
                              0
                              Это всё же несколько частный случай.
                              Для других (не ref struct) типов просто реализовать Dispose() и использовать в using нельзя.

                              PS: есть ещё async using & IAsyncDisposable
                                +2

                                Ну я к тому что Dispose теперь не прибит намертво к интерфейсу.
                                Насчет IAsyncDisposable, я не смог найти этому применение — ValueTask IAsyncDisposable() можно объявить для ref struct, но использовать в await using не получится — ref struct не работают в async методах. Только вызывать напрямую, от чего весь смысл теряется.

                            +1
                            Статья интересная. Хотелось бы уточнить один момент.
                            Этот синтаксис также можно использовать для вставки элементов в поле-коллекцию без публичного сеттера:

                            Вы акцентируете внимание на отсутствии публичного сеттера, но здесь важно различать инициализацию (читай, добавление элементов в) существующей коллекции и создание новой. С типом
                            class CustomType
                            {
                                public List<string> CollectionField { get; private set; } = new List<string>();
                            }
                            

                            Так можно
                            var obj = new CustomType { CollectionField = { "item1", "item2" } };
                            

                            И так можно
                            obj.CollectionField.Add("item3");
                            

                            А так нельзя
                            var obj2 = new CustomType { CollectionField = new List<string> { "item1", "item2" } };
                            
                            var obj3 = new CustomType();
                            obj3.CollectionField = new List<string>();
                            
                              0
                              Из злой магии еще вспоминается явная реализация интерфейсных методов a.k.a. EIMI.
                              interface I { void DoStuff(); }
                              
                              class Impl : I { public void DoStuff() { } }
                              
                              class EIMI : I { void I.DoStuff() { } }
                              
                              class Program
                              {
                                  static void Main(string[] args)
                                  {
                                      var impl = new Impl();
                                      impl.DoStuff();
                              
                                      var eimi = new EIMI();
                                      eimi.DoStuff();
                                      // 'EIMI' does not contain a definition for 'DoStuff' and no accessible extension
                                      // method 'DoStuff' accepting a first argument of type 'EIMI' could be found
                              
                                      var i = eimi as I;
                                      i.DoStuff(); // OK
                                  }
                              }
                              

                              Бывает, не сразу понимаешь, куда делся интерфейсный метод.
                                +1
                                А! Ну и множественная реализация методов же.
                                class Program
                                {
                                    interface A { void Method(); }
                                
                                    interface B { void Method(); }
                                
                                    class Impl : A, B
                                    {
                                        public void Method() => Console.WriteLine("I'm Impl");
                                        void A.Method() => Console.WriteLine("I'm A");
                                        void B.Method() => Console.WriteLine("I'm B");
                                    }
                                
                                    static void Main()
                                    {
                                        var impl = new Impl();
                                        impl.Method();
                                        (impl as A).Method();
                                        (impl as B).Method();
                                    }
                                }
                                

                                Выводит
                                I'm Impl
                                I'm A
                                I'm B
                                  0
                                  del (не туда)
                                  +1
                                  Кстати, к ветке, EIMI можно использовать со своими IDisposable, если хотите обозначить рекомендацию применять их в using'ах
                                  class EIMIDisposable : IDisposable { void IDisposable.Dispose() { } }
                                  
                                  static void Main()
                                  {
                                      using (new EIMIDisposable()) { } // OK
                                      new EIMIDisposable().Dispose(); // Compiler error
                                  }
                                  
                                    +1
                                    Ошибки в коде режут глаз…
                                    «Эквивалент» инициализации списков некорректен:
                                    Вместо
                                    var list = new List<int>();
                                    list.Add(1);
                                    list.Add(2);
                                    list.Add(3);
                                    

                                    должно быть
                                    var tmp = new List<int>(); // на самом деле переменная анонимная
                                    list.Add(1);
                                    list.Add(2);
                                    list.Add(3);
                                    var list = tmp;
                                    

                                    то есть связывание с переменной для полностью проинициализированного объекта (то же самое для инициализации свойств)

                                    Метод-расширение без this:
                                    public static class ExistingTypeExtensions
                                    {
                                        public static void Add<T>(/*!!!this!!!*/ ExistingType @this, T item) => throw new NotImplementedException();
                                    }
                                      +1

                                      Разницу между этими вариантами можно замтетитить только в многопоточном окружении, причем list должен быть членом класса доступным публично. Так что такое упрощение в целом допустимо. Но да, вообще знать надо, что инициализация атомарная.

                                        0

                                        Не обязательно в многопоточном окружении. Метод Add может выбросить исключение. Или обнулить поле, куда записывается ссылка на объект. Или ещё что-нибудь.

                                        0
                                        var tmp = new List(); // на самом деле переменная анонимная
                                        list.Add(1);
                                        list.Add(2);
                                        list.Add(3);
                                        var list = tmp;

                                        tmp.Add(1);
                                        tmp.Add(2);
                                        tmp.Add(3);


                                        Так же должно быть, вроде.
                                          0
                                          Сначала полная инициализация, потом связывание и никак иначе.
                                            +2
                                            Просто по иронии, когда Вы указали на ошибку, в Вашем примере, тоже закралась опечатка, вместо
                                            list.Add(1);

                                            должно быть
                                            tmp.Add(1);

                                            Иначе просто не скомпилируется.
                                              0
                                              Да, поторопился. Прощу прощения.
                                        0
                                        непонятно, почему декомпозиция деконструктором обозвана. и вообще глаз режет. у конструктора антоним деструктор, а это вообще какое-то непонятное слово.
                                          +3

                                          Постараюсь ответить:


                                          1. Декомпозиция обозвана деконструктором, т. к. деконструирует, "разбирает" значение в несколько переменных. Я использовал слово "деконструктор", т. к. по моему мнению так проще понять, что мы говорим о методе Deconstruct(...). Ну и в документации это тоже называют деконструктором.


                                          2. Деконструктор — это же не то же самое, что и деструктор, и вовсе не антоним конструктора. Тут деконструктор позволяет как бы "разобрать" объект и записать его части в несколько переменных, т. е. происходит деконструкция, "разборка". При этом исходный объект не меняется. Деструктор же отвечает за корректное уничтожение объекта, что совсем иное.


                                            0

                                            На самом деле деконструктор это как раз антоним конструктора, и в большинстве языков которые умеют в патмат они так и работают:


                                            data Foo = MkFoo { x :: Int, y :: Int }
                                            
                                            bar : Foo -> Int
                                            bar (MkFoo x y) = x + y

                                            Но в шарпе наслоения терминологии и просто каких-то старых решений, поэтому в нём некоторые вещи действительно уживаются немного странно.


                                            Деконструктор в шарпе — да, не антоним конструктора, но в языках откуда в шарп пришла эта идея — так и есть.

                                          0

                                          Инициализировать коллекцию можно не только элементом или перечислением. Можно сделать метод Add, принимающий любой тип и использовать значения этого типа в инициализаторе. Правда можно напороться на то, что компилятор не может определить, какой из Add-методов использовать, если их типы имеют что-то общее.

                                            +3

                                            Вот мне одному грустно от того, сколько магии в этом. Ну сделали бы хотя бы интерфейсик какой, чтобы не гадать "нужен вот такой специальный метод со специальным названием и специальной сигнатурой, и тогда вот такая штука заработает." Хотя, скорее всего так сделали, потому что большинство методов — статические, а в тайпклассы сишарп до сих пор не умеет.


                                            Жалко, что нет расширяемого способа делать такие штуки самостоятельно, а не рассчитывать на милость майкрософта.

                                              +3
                                              Жалко, что нет расширяемого способа делать такие штуки самостоятельно, а не рассчитывать на милость майкрософта.

                                              Я считаю наоборот. Хорошо, что такие штуки нельзя делать самостоятельно. Потому что это аццки весело, абсолютно во всех проектах пришлось бы разбираться с такими вот магическими поделками)

                                                +3

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


                                                У меня пример с растом перед глазами, где был сначала магический оператор ?, ну прям как в шарпе, а потом сделали нормальный Try-трейт, и оно теперь работает для кастомных типов.


                                                Хотел бы я чтобы в шарпе ? работал для моего типа Optional (ну типа Nullable, но не только для структур)? Вот очень хотел бы. Но такой возможности микрософт мне не дал.


                                                И ничего хорошего в этом я не вижу.

                                                0
                                                А Вас не смущает атрибут на объявлении базового класса атрибута? :)
                                                +2
                                                Пример из мира WPF:

                                                private DateTime m_now;
                                                
                                                public DateTime Now
                                                {
                                                    get { return m_now; }
                                                    set
                                                    {
                                                        if (m_now == value)
                                                        {
                                                            return;
                                                        }
                                                
                                                        m_now = value;
                                                        RaisePropertyChanged();
                                                    }
                                                }
                                                


                                                где RaisePropertyChanged:
                                                void RaisePropertyChanged([CallerMemberName] string propertyName = null);
                                                


                                                таким образом, если не передавать аргументов, то в качестве propertyName будет подставлено имя вызывающей функции или имя свойства. Подробности тут. Кстати, еще есть полезное ключевое слово nameof():
                                                RaisePropertyChanged(nameof(Now));

                                                Полезная штука, спасает от проблем при переименованиях. Без неё пришлось бы хардкодить имя свойства в виде строки.
                                                  –1
                                                  Без неё пришлось бы хардкодить имя свойства в виде строки.

                                                  Можно nameof(...) использовать.

                                                    +2
                                                    Увидел знакомую конструкцию — ужаснулся :)
                                                    Может быть, будет полезно: есть такие библиотеки как ReactiveUI и ReactiveUI.Fody. Про них есть статья здесь, на Хабре (вот комментарий по поводу Fody, также есть еще пару статей по ReactiveUI).
                                                    Используя их можно писать вот так:
                                                    [Reactive]
                                                    public DateTime Now { get; set; }

                                                    Это очень здорово экономит время и улучшает читаемость кода
                                                      +1
                                                      Весь бойлерплейт должен быть автоматизирован. Fody — хороший способ управления бойлерплейтом.
                                                        0

                                                        Пора писать статью "15 лет WPF: что изменилось" )

                                                        –1

                                                        Ну всё же идиоматически ваш код обычно записывается так:


                                                        private DateTime m_now;
                                                        public DateTime Now
                                                        {
                                                            get => m_now;
                                                            set => Set(ref m_now, value);
                                                        }

                                                        что вовсе не такой уж крокодил. Если ключевое слово field взлетит, то будет на строку меньше. А с пришествием original/replace будет просто атрибут, кто там хвалил Питон?

                                                        +1
                                                        Он может быть использован с любым типом, удовлетворяющим следующим условиям:

                                                        тип имплементирует интерфейс IEnumerable
                                                        тип имеет метод с сигнатурой void Add(T item)


                                                        Вопрос: а зачем понадобился IEnumerable у результата?
                                                          +1

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

                                                            0
                                                            Согласен. Правда это никак не спасает от «удивления», что указанных данных в коллекции не будет. Наконец, почему нельзя допустить коллекцию в которую можно положить и извлечь, а перебрать нельзя?
                                                              0

                                                              Ну это да, можно в нарушение семантики сделать что угодно. Можно сделать, чтобы Add ничего не добавляло, или добавляло не значение своего аргумента, а что-то другое. C# не может защитить программиста, который упорно вредит сам себе. Но по крайней мере C# старается, чтобы код, делающий странные вещи, и выглядел странно.
                                                              От коллекции программист на .NET ожидает, что она будет перебираться. А если хочется странного, так можно в GetEnumerator и исключение бросить.

                                                          0
                                                          Действительно мощные возможности для разработки DSL. В своё время в .NET-среде только Nemerle и Clojure-CLR позволяли делать нечто подобное, земля им пухом.
                                                            0
                                                            А какие ограничения не позволили в LINQ завезти left join и произвольный предикат соединения таблиц, а не только эквивалентность?
                                                              0

                                                              Вместо left join в LINQ есть group join, это связано с тем, что group join лучше ложится на объектные запросы.


                                                              Произвольный предикат же запретили, видимо, потому что для коллекций объектов в памяти соединение по произвольному предикату будет существенно медленнее; а когда он всё-таки нужен — его всегда можно переписать через вложенные запросы.

                                                            Only users with full accounts can post comments. Log in, please.