Как стать автором
Обновить

Комментарии 70

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

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


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


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

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


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

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

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

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


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

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

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

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

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


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].

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

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

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

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

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


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

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

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

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

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


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

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


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


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

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


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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

Вы акцентируете внимание на отсутствии публичного сеттера, но здесь важно различать инициализацию (читай, добавление элементов в) существующей коллекции и создание новой. С типом
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>();
Из злой магии еще вспоминается явная реализация интерфейсных методов 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
    }
}

Бывает, не сразу понимаешь, куда делся интерфейсный метод.
А! Ну и множественная реализация методов же.
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
Кстати, к ветке, EIMI можно использовать со своими IDisposable, если хотите обозначить рекомендацию применять их в using'ах
class EIMIDisposable : IDisposable { void IDisposable.Dispose() { } }

static void Main()
{
    using (new EIMIDisposable()) { } // OK
    new EIMIDisposable().Dispose(); // Compiler error
}
Ошибки в коде режут глаз…
«Эквивалент» инициализации списков некорректен:
Вместо
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();
}

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

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

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);


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

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

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

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


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


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


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


data Foo = MkFoo { x :: Int, y :: Int }

bar : Foo -> Int
bar (MkFoo x y) = x + y

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


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

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

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


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

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

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

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


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


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


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

А Вас не смущает атрибут на объявлении базового класса атрибута? :)
Пример из мира 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));

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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


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

Произвольный предикат же запретили

Можно где-то почитать об этом запрете? Не хотелось бы спалиться на потреблении запрещённых технологий.

Его просто нет в Linq API и синтаксисе. Какие ещё подробности вам нужны?

Простите! Я просто спутал предикат с квантификатором (и часто ловлю баги на этом).

Можно еще создать свой async Task-like тип, если вас не устраивают Task. Переопределить в нем execution flow, убрать потоки, шедулер.


Например, я их использую как более мощную и компактную замену Behaviour Trees и корутин в разработке игр, например так:


if (await Pursue() && await Kill())
    await VictoryDance();

Минимальный код:


[AsyncMethodBuilder(typeof(MyTaskBuilder))]
class MyTask {
    public MyTaskAwaiter GetAwaiter() => throw new NotImplementedException();
}

class MyTaskAwaiter : ICriticalNotifyCompletion {
    public void GetResult() => throw new NotImplementedException();
    public bool IsCompleted { get => throw new NotImplementedException(); }
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public void UnsafeOnCompleted(Action continuation) => throw new NotImplementedException();
}

class MyTaskBuilder {
    public MyTask Task { get => throw new NotImplementedException(); }
    public static MyTaskBuilder Create() => throw new NotImplementedException();
    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => throw new NotImplementedException();
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => throw new NotImplementedException();
    [SecuritySafeCritical] public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine => throw new NotImplementedException();
    public void SetResult() => throw new NotImplementedException();
    public void SetException(Exception e) => throw new NotImplementedException();
    public void SetStateMachine(IAsyncStateMachine stateMachine) => throw new NotImplementedException();
}

namespace System.Runtime.CompilerServices {
    public sealed class AsyncMethodBuilderAttribute : Attribute {
        public AsyncMethodBuilderAttribute(Type builderType) => BuilderType = builderType;
        public Type BuilderType { get; }
    }
}

Я ещё добавлю, что вот эта портянка с AsyncMethodBuilder нужна только ради возможности делать метод async. А вот await-ить можно вообще всё, что у чего есть метод GetAwaiter (включая extension-ы).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории