Pull to refresh

Comments 60

ИМХО primary constructor сделан исключительно ради облегчения труда программистов при работе с DI.

Замена простыни

public class SomeService
{
  private readonly Service1 service1;
  private readonly Service2 service2;
  ...  
  private readonly Service100500 service100500;

  public SomeService(Service1 service1, Service2 service2, ... Service100500 service100500)
  {
    this.service1 = service1;
    this.service2 = service2;
    ...
    this.service100500 = service100500
  }
}

на

public class SomeService(Service1 service1, Service2 service2, ... Service100500 service100500)
{
 
}

многого стоит.

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

ИМХО primary constructor сделан исключительно ради облегчения труда программистов при работе с DI.


Отчасти согласен, отчасти нет

Возьмем один кейс, который забыл в статью включить - валидационная логика в конструкторе, ее негде писать. Если мы захотим сделать исключение при null значениях параметров, нам снова придется делать обычный конструктор, т.к нет init{} блока.

Также, в вашем примере (2-ая секция кода), сервисы не будут readonly. Да, согласен, маловероятно что их кто-то поменяет, но лично мое субъективное имхо, фича должна сразу грамотно проектироваться, чтобы например кейсы с иммутабельностью учитывать

сервисы не будут readonly

Да, это косяк, но с другой стороны - вы их прописали в конструкторе, зачем их пересоздавать в коде?

Может в дальнейшем пофиксят

Если мы захотим сделать исключение при null значениях параметров

Именно в случае работы с DI это лишнее, т.к. DI гарантирует нам наличие запрошенного сервиса в общем случае. Синтаксис с первичным конструктором облегчает кодописание для 99% классов, подключаемых через DI.
В остальных случаях лучше использовать стандартный конструктор.

Именно в случае работы с DI это лишнее

Ну, если классу нужно знать, кто в его конструктор будет пропихивать аргументы, то попахивает «фигак-фигак и в production», что, впрочем, по моему скромному мнению, и есть стиль, максимально одобряемый Microsoft.

Вроде бы, валидация предусмотрена, а для readonly-свойств в структурах (и readonly-структур) создаются immutable-поля (правда, похоже, в этом механизме пока что есть баг).

Many types require more specific validation on constructor parameters. For example, the BankAccount has specific requirements for the owner and accountID parameters: The owner must not be null or whitespace, and the accountID must be a string containing 10 digits. You can add this validation when you assign the corresponding properties:

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = ValidAccountNumber(accountID) 
        ? accountID 
        : throw new ArgumentException("Invalid account number", nameof(accountID));

    public string Owner { get; } = string.IsNullOrWhiteSpace(owner) 
        ? throw new ArgumentException("Owner name cannot be empty", nameof(owner)) 
        : owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";

    public static bool ValidAccountNumber(string accountID) => 
    accountID?.Length == 10 && accountID.All(c => char.IsDigit(c));
}

Это ничего не стоит, потому что 90% кодовой базы использует private readonly _field; как codestyle полей, а primary constructors этот codestyle ломают.

Что мешает в primary constructors объявить поля как _field? Единственный нюанс, как выше написали, они не будут readonly, но в 99% случаев работы с DI на это по барабану.

что мешает кодстайл поменять?)

Это бы помогало, если бы их можно было бы сделать только для чтения, а так надо ещё добавлять поля или свойства.
А после добавления полю/свойства у тебя в любом месте есть доступ и к полю/свойству что правильно. Так у тебя все ещё остаётся доступ и параметру первичного конструктора, это прямо бесит, потому что легко перепутать...
В общем получилась какая то какашка, хотя я сначала думал что это будет прямо бомба.

В чем принципиальность, чтобы поле было readonly?
Я использую их прямо так, не объявляя лишних полей/свойств.
И желания переобъявить условный DbContext внутри сервиса не появляется.

Сейчас прямо попробовал.
Если сделать так, то доступа к параметру первичного конструктора не будет

public class SomeClass(AppDbContext db)
{
    private readonly AppDbContext db = db;
}

Костыль, конечно

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

А по поводу отличия class/struct vs record надо читать документацию по языку, а не полагаться на интуицию и предположения из другого мира...

А по поводу отличия class/struct vs record надо читать документацию по языку, а не полагаться на интуицию и предположения из другого мира...

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

Вот вроде бы такие синтаксические плюшки должны помогать удобством, но чаще всего просто становятся обузой. С вероятностью в 80% ты её использовать не будешь, а учить надо, ведь кто-нибудь да напишет так, а ты не знаешь. :\

Такая же фигня и C++ начинающим смахивать на PL/I.

Этот вектор развития стал моей причиной ухода с шарпов. А жаль. Я сдиел на них буквально с детства. Помню ещё фреймворк версии 1.1 и то, каким офигительным тогда казался второй фреймворк. Помню свой первый профессиональный проект, за который я получил первую в жизни зарплату, написанный на MS .NET Framework 2.0 и шарпах. Всё было на ASP.NET со всеми новейшими плюшками. Да, это работало только на Винде и только под IIS, но работало стабильно и круто.

Шарпы ещё тогда славились "многословностью", но она была намного менее многословной чем Ява. Но эта самая многословность отличала шарпы от других (сосбенно скриптовых) языков. Всё было чётко и понятно. У тебя не было двусмысленности. Тебе не надо было перепроверять, что значит знак =, или какого типа у тебя переменные.

Я с большим недоумением смотрел на конструкцию var. Я вообще не понимал, почему её ввели. Для объявлений типа int t = 3 она была достаточно смешной. Для более сложный кейсов, когда тебе надо было объявить Class.Subclass.SubSubClass t = new Class.Subclass.SubSubClass() intellisence отлично справлялся с работой.

Да, понятно, нам хотелось сделать так, чтобы LINQ работал попроще. Поэтому мы начали делать type inference и всё такое.

Самая жесть началась с 2015 года, когда Скотт Хансельман объявил о том, что ASP.NET перешёл в ОpenSource и стал доступен на гитхабе. Шарпы последовали за ним, как и вся платформа. После этого началась дикая каша с названием множества фреймворков и версий продукта. .NET, Core, Mono и все остальные начали беситься.

С этого же времени в шарпы начали добавлять функциональность все кому не лень, в попытках "облегчить" этот язык. Но проблема в том, что ему изначально не надо было быть "лёгким". Он был разработан для взрослых дядек, которые пишут проекты по 4 года, и для который написание hello world занимает 2 недели, чтобы пробиться через всю бюрократию. Но в этом и была прелесть языка, особенно после всех ужасов яваскрипта, где у вас есть var, let и const, и наличиствуют приколы типа == и ===, с добавлением возможностей типа console.log({} + []); console.log([] + []); console.log({} + {}); console.log({} - []); console.log([] - []); console.log({} - {});Кстати, не отказывайте себе в удовольствии, вставьте этот код в консоль вашего браузера, и попробуйте предугодать ответы, которые вы увидите на экране.

Зачем надо было тратить усилия, для того, чтобы позволить запускать код без int Main()? Ну да, мы получили что-то что ещё больше похоже на Node.js. Но смысл в этом был какой? Каждая IDE для шарпов всегда начинала с того, что создавала Program.cs с int main. Но нам же нужен синтаксический сахар. Мы будем убивать boilerplate.

В шарпах всё было разложено по ящичкам, ящички были разложены по полочкам и приварены к полочкам, полочки были вкручены в стены десятью болтами на 30, стена была железобетонной, толщиной в 10 метров, и установлена на фундаменте в 50 метров в глубину. Сейчас мы начали добавлять "синтаксический сахар" в шарпы. Очень умное название. Ложечка сахара делает кофе намного более вкусным. Две ложечки заставляют тебя задуматься о том, надо ли тебе это. Три - и кофе уже приторный. Мешок сахара - и ты понимаешь что из этой комнаты тебе выходить только вперёд ногами.

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

В своё время самым сложным вопросом на собеседовании в шарпах было "расскажи про разницу между public, private, internal, protected и т.п." Сейчас у тебя есть вообще чёрт знает что (как хорошо показано в статье). И всё это сделано для того, чтобы сомнительный паттер dependency injection смог работать. Хотя, жить можно было и без DI. Голанг отлично без него живёт. Зато всё прикручено к полу болтами. Конструкции не имеют неясных значений.

Программирование - это очень детерминированная деятельность. Тебе надо дать чёткие инструкции для процессора, чтобы получить ожидаемый результат. Сейчас инструкции становятся всё более и более размытыми. И самое главное - я не могу понять, почему это происходит.

Что самое интересное, в мире, где все пишут с ChatGPT на пару эта boilerplate вообще не имеет никакого значения. Если тебе влом писать CRUD для класса, настрочи один метод, и попроси бота написать остальное. При этом ты будешь ясно видеть, если бот написал фигню. И это не занимает много времени.

Согласен со всеми ваши словами. Плюс к этому - вроде столько плюшек в студии, столько функций в языке, а открываешь приложения, и любая самая безобидная программа весит как "вчера" - операционная система, база данных и ещё куча чего вместе взятые, и отклик плохой, и зависания постоянно итд.

На что в итоге перешли если не секрет ?

var нужен для анонимных типов. А анонимные типы нужны были Linq, а без linq шарп не шапр.

Запуск без Main, глобальные юзинги и обобщённая математика нужны для ML. Догоним и перегоним питон типа. Мне ML никуда не упёрся, но и не мешает.

А свич кейсы лично мне нравятся, стало удобнее.

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

А в C# все хорошо. У кого то новые версии вызывают батхерт и крики "в наши годы такого не было", а мне например нравится как из моего кода с каждой новой версией убирается ненужные более строки, не нарушая при этом целостность программы.

После перехода на 12 версию мне больше не надо будет объявлять для каждой зависимости локальную переменную, параметр конструктора и еще в самом конструкторе их совокуплять. Я все смогу записать одной строкой вместо трех.

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

ЗЫ: почему то больше всего ругают нынешний шарп те кто видел его последний раз когда то давно.

Соглашусь. Так и хочется таким сказать - а теперь напишите на .Net 2.0 который не кор какой нибудь проект побольше чем просто CRUD.
А по поводу конструктора - все таки соглашусь с автором статьи - фича не доработана. Ну то есть задача "что-то сделать с DI" - окей, мы решили проблему вот так. Но... как бы объяснить... она просто не проработана. Ну то есть ее выкатили только для одной вещи, хотя можно было еще чуть чуть посидеть и сделать ее удобной не только для DI но и для всех остальных кейсов (чтобы поля сразу были ридонли/протектед/паблик) и тогда можно было и всесь остальной код переписать. Не только с DI. Вот к чему основная проблема. Надеюсь что все таки это доделают но то что сейчас не сделали по умолчанию ридонли - в будущем аукнется.
ЗЫ, не приверженец джавы и в целом нравиться развитие языка. Когда познакомился с Котлин - очень грустил что в шарпах нет аналога дата классов

Соглашусь. Так и хочется таким сказать - а теперь напишите на .Net 2.0 который не кор какой нибудь проект побольше чем просто CRUD.

.NET "который не кор" называется .NET Framework. В .NET Framework 2.0 не было много чего реально полезного для написания самой логики программы, а не только сокращения числа букв. Например - современной асинхронности: не только async/await, но и Task Parallel Library (Task и пр.). И фреймворка MVC с шаблонами Razor тоже не было. Все это появилось в только .NET Framework 4.5 (ну да, уже 10 лет назад - но не 18) Так что желание ваше сказать - оно чрезмерно утрировано.

PS Ну, а идея сделать поля readonly - она больше для парадигмы ФП свойственна, чем для ООП - в котором объект как правило имеет изменяющееся состояние. Ну, а C# - язык куда более пригодный для работе в парадигме ООП как в подвиде имеративной парадигмы, чем в парадигме ФП. Это проявляется хотя бы в том, что в C# невозможно гарантировать, что параметр типа функции (то есть, в перевлже на C#, делегат) не имеет побочных эффектов (и, к примеру, в коде ASP.NET Core есть места, в которых и спольуются делегаты с побочными эaфектами). А потому все связанные с этим преимущества ФП в C# недоступны. Видимо поэтому неявные поля, определяемые через первичный конструктор, были сделаны в парадигме ООП - изменяемыми.

Вы говорите свойственно ФП, по откуда тогда рулы чтобы помечать инджекнутые сервисы как ридонли? Авторы сами же сказали - замена инджекта через конструктор. Ну окей. тогда где ридонли? ну потому что замена получается неполной. И всего лишь. Тут нет про ООП или ФП

Авторы сами же сказали - замена инджекта через конструктор.

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

Все это появилось в только .NET Framework 4.5

3.5 же

Таки нет: Task Parallel Library (Task и пр.) - в .NET Framework 4.0, async/await, с его ключевым методом GetAwaiter - в 4.5. Всё это несложно проверить и сейчас, по документации на сайте MS.

Но за MVC с Razor таки не скажу: не следил за перипетиями появления MVC, перехода с aspx на cshtml и пр.

непонятно вы шарп ругаете или голанг защищаете лол

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

Я эту тенденцию заметил за языками и проектам, которыми управляют не с гитхаба а из централизированного коммитета. В ГО генерики приходили десятилетиями, и то только после долгих раздумий. Именно из за централизированного управления язык сохраняет совместимость.

То же самое было с ядром линукса. Пока жив сам Торвальдс, линукс будет относительно стабильным. Но я помню как с пяток лет назад кто-то внёс предложение убрать бинарную совместимость, чтобы "перевести ядро на новые рельсы". Торвальдс тогда такое ответил, что страшно стало.

Проекты живут лучше, когда есть один основатель. (Будь то человек или коммитет) и когда этот основатель следует целям и принципам, которые он установил в начале проекта. Иначе выгодит npmjs.org

Ну, судьбе PL/1 совсем не помогло то, что он разрабатывался в рамках одной корпорации. Так что, похоже, "здесь все не так однозначно"(с).

Ну кстати в третьей скале произошли довольно радикальные изменения.

Там и extension методы, которые раньше делались через implicit, и тайкпклассы через given/using (которые раньше были тоже implicit), и inline методы, которых раньше не было (была только аннотация@inline, но она иначе работала).

И вдобавок в третьей скале до сих пор нет и возможно и не будет @speciaized для классов. Именно для них новый inline заменой не будет, в условном Vector[T](x: T, y: T) можно было указать дженерик тип как специализированный, чтобы сгенерировался специальный подкласс, например, с Double полями без боксинга. Конкретно мне эта фича была бы нужна кое-где.

Но правда изменения в скале выглядят на несколько порядков более продуманными и логичными

Primary constructors в C# выглядят как какая-то диверсия, хотя казалось бы просто бери удачное решение из других языков и делай так же.

Интересно, кто-нибудь заметит сарказм в вашем комментарии?

Если говорить про сторонние библиотеки, то да, сарказм тут уместен. Но все ж сам язык 2й версии довольно стабилен.

С тем же примерно успехом я могу ссылаться на стабильность какой-нибудь седьмой версии сишарпа.

А скала стала себе славу языка с ломающими изменениями в минорных версиях

Вот казалось бы, как здесь можно напортачить? Идея взята прямиком из Kotlin, все что надо было сделать это перенести известную, успешно работающую функциональность из одного языка в другой. Всё.

Как говорится, воруй как художник? Думаю, что это не про Primary Constructors, потому что насколько плохо своровать фичу это надо было постараться.

Как по мне, то в статье неверно изложена причина появления первичных конструкторов (Primary Constructors) в C#
Первичные конструкторы в C# - это не про "своровать фичу" (то есть функцию, если по-русски) из совсем дуругого языка, а про естественное развитие C# в выбранном его создателями направлении.

Мой многолетний взгляд на нововведения в C# привел меня к мысли,что это за направление: это увеличение лаконичности языка. Многие добавленные конструкции, такие как var в описателях , new() (без указания типа), код верхнего уровня (не включаемый в методы), определение тел методов, геттеров и сеттеров как стрелочных функций - они бьют именно в эту точку: позволяют сэкономить нажатия на клавиши при написании кода с той же само функциональностью. А заодно - писать меньше строк текста, улучшая обозримость программы. Первичные конструкторы, как они реализованы в C# 12 тоже направлены именно на это, на увеличение лаконичности: они позволяют сэкономить на размере кода - и самого конструктора, и на описаниях внутренних полей, используемых в методах. Потому они и сделаны так, минималистично. И эту задачу они, в целом, выполняют. Для других же задач - типа создания альтернативного способа полноценного, с областями видимости и пр., описания полей объекта - они не предназначены.
Единственный неочевидный выбор, который был сделан при создании этого расширения - это делать ли эти скрыте поля, которые могут понадобиться методам, неизменяемыми. В целом, решение тут неоднозначное, потому что поля в языке C# могут использоваться и так, и так, поэтому надо выбирать. Решение же добавлять возможность выбора, полагаю, было отвергнуто именно из соображений лаконичности. Созадатели языка решили - не делать, вероятно - потому что в C# все эти неизменяемые поля - это более поздние нововведения, а традиционно поля были изменяемыми.

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

Лаконичность языка в первую очередь работает на скорость чтения того что написано, а не на написание нового.

Где то когда-то читал что один из аргументов писать открывающуюся фигурную скобку на той же строке (как в жаве) а не на новой (как в С#) - это возможность уместить больше кода на одном экране и соответственно быстрее просмотреть его по диагонали.

Лаконичность языка в первую очередь работает на скорость чтения того что написано, а не на написание нового.

Это да.
Однако введение новых сущностей в язык ради этой самой лаконичности работает против скорости чтения: все эти сущности предварительно надо как следует заучить. То есть, потратить на это время, которое пригодилось бы для чего-нибудь ещё полезного или приятного.
И одно дело - сделать это один раз, при освоении языка, а совсем другое - тратить на это время каждый год.

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

Претензия к языку что он развивается и новые фичи надо учить прежде чем использовать - как минимум странная. Судьба программиста есть бесконечное изучение чего то нового, если это только не программист на коболе.

Разбиение кода на функции может навязывать не какой то там стильномолодежный стиль, а здравый смысл. Если его нет то могут быть обе крайности, в том числе и методы по 500 строк с копипастой. Это уж точно не к языку претензии.

А он точно именно развивается? То, что я вижу, скорее характеризуется словом "раздувается": добавляются, в основном, все новые способы выразить одну и ту же семантику, которую можно было выразить и старыми средствами. Так что я бы с легкостью обошелся без большинства этих "фич", но - не получается, потому что надо читать код, написанный другими. Например, в своей крайней статье я приводил пример вот такого кода (из библиотеки языка), с которым мне надо было разобраться:

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value)
{
    using ICacheEntry entry = cache.CreateEntry(key);
    entry.Value = value;

    return value;
}

Вот, положа руку на сердце, скажите, в чем польза "фичи", которая позволяет сэкономить тут пару фигурных скобок после оператора using? Зачем мне ко всему прочему запоминать дополнительное правило, что если эти фигурные скобки не указаны, то using распространяется на код до конца блока?

Ну, а разбиение кода на методы, с типичным стильномодномолодежным требованием "не более 10 строк на метод" - это каким здравым смыслом обосновывается? Количество последовательных строк, которые можно держать в голове - он, во-первых, у каждого свой, а, во-вторых, вполне поддается увеличению путем тренировки. Я, конечно, не настоящий программист, и не пишу циклы DO на 5 страницах (т.е. в 300-350 строк кода), но пару десятков строк держу в голове запросто, плюс ещё и простые в обращении клавиши PgUp/PgDn на помощь приходят. А вот если семантика кода раскиндана по куче методов в куче файлов, то там уже все сложнее.

Короче, я направлением модификации языка C# недоволен, и свое недовольство я тут выразил. Но это - мое недовольство: я, максимум, предлагаю другим его разделить, если согласны, но не пытаюсь навязать.

Впрягусь: юзинги без скобок нужны, когда их много, чтобы код не сдвигался слишком сильно вправо. Ещё часто ресурс создаётся в начале и уничтожается в конце функции. Тут юзинги без скобок тоже подходят.

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

Это если один за другим.

А бывает и не так.

using var a = GetA();

Log.Write(“Start”);

using var b = GetB();

Тут также следует отметить разную семантику о которой частенько забывают.

В случае с блоком ресурс освободится сразу в конце блока, а без него только в конце функции.

Тут надо смотреть в оба, что действительно подходит.

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

Правильно ли я понимаю, что Ваш посыл в том, что две схожие конструкции - это плохо, потому что надо уметь выбирать какую из них использовать, и поэтому то, что ввели вторую - это плохо?

Я не говорил, что введение простого using это плохо.
Как раз это очень удобно.

using без скобок - время жизни до конца метода. using со скобками - до конца блока. Что здесь может напрягать при понимании - я вообще не понимаю.

А вот что код будет компактнее при чтении и удобнее (и это главное) при изменении - это важнее.

Если писать короткие методы, которые делают что то одно (но делают это хорошо :) ) то в большинстве случаев using без скобок приносит реальную пользу.
Но если вы тренированный и держите в голове метод на два экрана (причем не только в момент написания, а вечно), то у вас там конечно может собраться пяток-другой таких юзингов, которым нужны скобки.

Но (исключая некоторые случаи) это write-only код, с которым потом будет много проблем.

using без скобок - время жизни до конца метода

Это утверждение не является верным. Остальные размышления, сделанные из неверных предпосылок, также являются неверными.

Пруф

Ну да, до конца блока, описка. Как это делает неверным остальной комментарий?

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

Но (исключая некоторые случаи) это write-only код, с которым потом будет много проблем.

Вот эти тезисы стали выводами из ложных предпосылок. Поскольку "время жизни" объекта - это блок using-а, то если таких блоков будет много подряд (что само по себе code smell), то всё равно накопления не будет.

using без скобок - время жизни до конца метода

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

А вот что код будет компактнее при чтении и удобнее (и это главное) при изменении - это важнее.

Сложно изменять код, который не понимаешь. А разбухший синтаксис со множеством фич - это не то, что пониманию помогает: его надо прежде выучить, а потом ещё регулярно обновлять новыми фичами. Мне лично тратить на это время удовольствия не доставляет. Примерно так же, как не доставляет и удовольствия тратить время и силы на освоение новой версии программы: тут статья была сегодня, и я вижу в ней много параллелей с обсуждаемой здесь темой о разбухании языка.

Если писать короткие методы, которые делают что то одно (но делают это хорошо :) ) то в большинстве случаев using без скобок приносит реальную пользу.

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

Но (исключая некоторые случаи) это write-only код, с которым потом будет много проблем.

Объективно код write-only не бывает: это - характеристика, завяисящая в значительной мере от квалификации читающего и желающего модифицировать. А заучивание новых фич конкурирует за время и силы, потребные для повышения квалификации.

Недавно спрашивал про primary constructors у Медса Торгенсена. Политика такая: они перешли на более частые и более гранулярный релизы. У них несколько вариантов дальнейшего развития primary constructors (и других фичей), но ни одна из реализаций не лишена изъянов. Они выпускают минимальную версию в прод и ждут фидбек. На основе фидбека решают какая из реализаций удовлетворяет большинсиво разработчиков. Так что и блоки инициализаторов и управление уровнями доступа или изменяемостью/неизменностью - все это может появиться в следующих версиях языка. С чего может реально пригореть и что может поменять много идиом C# - это ключевое слово extension, но с ним ясности нет пока.

Да, так и хочется восклинуть: "Горшочек, не вари!"

В мире Java довольно долго считали, что "много новых фичей в языке - это от лукавого", а потом появился Котлин и откушал часть рынка...

Это - другая крайность. А истина редко бывает совсем с краю.

И вообще, функциональности (то бишь, features) бывают разные. Есть те, которые увеличивают возможности языка, позволяют существенно упростить написание кода и снизить вероятность внесения ошибок. В C# это, к примеру, обобщенные типы или async/await. Такие очень полезны, да.
А есть такие, которые всего лишь уменьшают число нажатия на клавиши при написании. Типичный пример из современного C# - это соглашение, что после using фигурные скобки можно не ставить, и он будет действовать до конца блока. При том, что сам по себе using ближе к первому типу - позволяет не забыть вызвать Dispose() - с непривычки читается такой "оптимизированный" код ужасно, да и ошибку внести можно. Мое мнение, что без таких функциональностей стоит все же обойтись. И первичные конструкторы - это IMHO как раз из той же серии экономии на нажатиях клавиш.

А вообще, давным-давно придумана возможность писать разные модули на разных языках (и в .NET она есть). Так что, я думаю, что если кого привлекает своей свободой JS или, там, Python, то им надо дать возможность писать на любимом языке для .NET, и будет им счастье не за счет других.

PS Кстати, лично мне вот в C# с некоторых пор стало не хватать операции "запятая" - той самой, освященной ещё традииями языка C: очень удобно использовать ее для написания лямбд, много нажатий экономится (фигурные скобки, return и т.д.) - а лямбдам очень идет быть короткими.
Но, вспоминая молодые годы и типичные для виртуозов C тех времен однострочные выражения, через которые приходилось буквально продираться, я все же думаю, что операции "запятая" в C# не надо.

А мне нравятся короткие неймспейсы и юзинги без скобок. Надо было сразу так делать вообще. А с primary constructors будет скорее всего как с наллабилити, которую в трех версиях фактически релизили: сейчас без более продвинутых возможностей у них очень ограниченное применение. ИМХО их дальше будут обвешивать

Что-то вы как-то все напутали.

  • В примере 1 у меня получается false, а не true, что я делаю не так? Ваше заявление, что если поле не используется, то оно не сохраняется, не верно.

  • В примере 2 вы зачем-то смешали primary constructor и immutability, как это связано? Существуют readonly struct, но, вообще-то, никто не обещал, что вместе c primary constructors введут еще и возможность ограничения классов на изменение

  • По уровням доступа - просто используейте обычные конструкторы, а не primary, всегда можно указать все явно. Primary constructor это синтаксический сахар, который не создан, чтобы покрывать все возможные кейсы полного синтаксиса.

  • В примере 3 вообще непонятна претензия, init отдельно, primary constructor отдельно. Вы сделали декларацию класса с primary constructor вместе обязательным полем с object initializer и удивляетесь, почему компилятор не позволяет вам использовать только одно из двух, называя это почему-то багом.

В документации же явно написано: primary constructor создает поля, доступные внутри описания класса. Это синтаксический сахар, которые покрывает простые кейсы для сокращения количества кода.

Многое, что делали с языком после С#6-7 - это порча хорошего и стройного языка и превращение его в джаваскрипт. Жабаскрипт такой только по историческим причинам, а тут сознательно портят язык.

Для верхнеуровневых языков скорость разработки зависит не от количества синтаксического сахара или того, пишешь ты конструктор за 30 символов или за 40, а от простоты и наглядности кода, возможностей выражать понятия доменной модели в коде наиболее близким способом, не увеличения, а лимитирования способов сделать одно и то же, ограничения возможностей "писать, что попало" и структурирование всего кода в определённые рамки, близость к натуральным языкам ддя простоты понятия и оперирования...

Основной тех. долг, который несёт косты для бизнеса это ошибки в проектировании доменной модели, макаронный код, проблемы времени жизни объектов и т.п., а не лишняя строка для определения пропсы.

Так ругались еще тогда когда в С# появились автопроперти. Потом оказалось что ничего плохого в этом нет. И так каждую версию....

Простота и наглядность кода определяется в том числе и количеством бойлерплейта. Меньше - нагляднее. В этом направлении и делаются подобные изменения.

А то что непривычно такое читать - так это вопрос времени.

Соглашусь с автором - primary constructor'ы классов/структур это просто жуткий провал в архитектуре синтаксиса C#. Самое главное, что он противоречит аналогичному синтаксису record. До этого я считал самым неудачным дизайном именно record - т.к. они не совместимы с классами - т.к. их нельзя отнаследовать от класса - это же было фиаско - технически же там нет никаких ограничений - "позади" record стоит просто кодогенерация - можно было бы и просто аннотацией обойтись (почти как в Kotlin - там правда вместо аннотации ключевое слово - но это не принципиально - всё равно будет определён class и никаких ограничений на наследование и конструкторы не накладывается; в C# даже такое определение как "record class" - это всего-лишь банальный "record") - но record это всё-таки другая тема, а C# 12 primary constructor это куда больший провал. Но, в будущем его ещё можно попробовать исправить - всё-таки введя возможность вставлять модификаторы доступа (+ readonly) в параметры primary constructor'а - если есть - то создавать поле.

Кстати, у record тоже есть аналогимчный косяк с параметрами primary constructor

//Вариант 1
public record Test(int A) 
{ 
  public int A; 
}

//Вариант 2
public record Test(int A) 
{ 
  public int A { get; set; } 
}

//Вариант 3
public record Test(int A)
{
	public readonly int A = 0; //Без инициализатора нельзя
}

var t1 = new Test(10);
var t2 = new Test(222222);
var t3 = new Test(3);

Console.WriteLine(t1.Equals(t2)); //true
Console.WriteLine(t1.A); //0
Console.WriteLine(t2.A); //0
Console.WriteLine(t3.A); //0

Хотя, насколько я помню, при анонсе record говорили что так можно задавать доступность свойств, генерируемых по первичному конструктору.

Поэтому для record надо так же писать

//Вариант 1
public record Test(int A) 
{ 
  public int A = A; 
}

//Вариант 2
public record Test(int A) 
{ 
  public int A { get; set; } = A
}

//Вариант 3
public record Test(int A)
{
	public readonly int A = A;
}

var t1 = new Test(10);
var t2 = new Test(222222);
var t3 = new Test(3);

Console.WriteLine(t1.Equals(t2)); //false
Console.WriteLine(t1.A); //10
Console.WriteLine(t2.A); //222222
Console.WriteLine(t3.A); //3

Так что первичный конструктор классов не так уж сильно отличается от первичного конструктора записей - но всё же отличие есть (отсутствие автогенерируемый свойств) - и это очень существенно!

особенно когда можно писать так


public record class Test1(int A) { }
public class Test2(int A) { }

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

ну и такая вещь как

public class Test(int A)
{
	public int A {get; set;} = A 
    
    //как и такая
    //public int A {get; set;} 
}
var t = new Test(A:1) {A = 2};
Console.WriteLine(t.A); //2

Мне кажется дикой - на мой взгляд тут нужно генерировать синтаксическую ошибку - прочем не будь это первичным конструктором (а обычным), то так тоже можно было делать - и ранее это никого не смущало - можно воспринимать как мутацию класса при его инициализации (и тут даже есть своя логия и польза от этого)

Кстати это работает аналогично

public record class Test (int A)
{
	public int A {get; set;}
}

var t1 = new Test(A:10){A = 1};
var t2 = new Test(A:222222){A = 1};

Console.WriteLine(t1.Equals(t2)); //true
Console.WriteLine(t1.A); //1
Console.WriteLine(t2.A); //1

Кстати, рефлексия в обоих случаях (record или class)

var mt = t1.GetType();
foreach (var f in mt.GetFields())
    Console.WriteLine($"field: {f.Name}");
foreach (var f in mt.GetProperties())
    Console.WriteLine($"property: {f.Name}"); //Вернёт только свойство A

Так что у record это тоже провал, хоть у чуть меньший. И до сих пор никак это не поправлено!

Поэтому я и решил создать свою версию компилятора для .NET на базе Roslyn - где всё как в Kotlin (ну кроме suspend функций - асинхронность как в C#)

Sign up to leave a comment.

Articles