Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.12.25 13:31
Оценка:
Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.

И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку:
var obj1 = new SomeType1() { Prop1 = null! };
var obj2 = new SomeType2() { Prop1 = obj1 };

obj1.Prop1 = obj2;


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

Предлагаю расширить C# блоком инициализации, внутри которого не будет проверяться нулабельность, а все проверки будут осуществляться при выходе из него:

init (var obj1 = new SomeType1()) // не ругается на то, что obj1.Prop1 не заполнен
{
    var obj2 = new SomeType2() { Prop1 = obj1 };

    if (condition)
        return; // компилятора ругается, так как к этому моменту obj1.Prop1 не проинициализирован.

    obj1.Prop1 = obj2; // obj1.Prop1 считается проинициализированным.
}


Компилятор осуществляет проверки нулабельности только при выходе из блока (в конце или по return).

Предполагается, что можно объединять несколько инициализаций:
init (var obj1 = new SomeType1())
init (var obj2 = new SomeType1())
init (var obj3 = new SomeType1())
{
    obj1.Prop1 = obj2;
    obj2.Prop1 = obj3;
    obj3.Prop1 = obj1;
}


Так же предполагается, что синтаксис аналогичен using, т.е. если не указаны круглые скобки, блоком является вложенная область видимости.
void Foo()
{
    init var obj1 = new SomeType1();
    init var obj2 = new SomeType1();
    init var obj3 = new SomeType1();
    obj1.Prop1 = obj2;
    obj2.Prop1 = obj3;
    obj3.Prop1 = obj1;
}
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Отредактировано 17.12.2025 20:25 VladD2 . Предыдущая версия .
Re: Идея для новой версии C#
От: BlackEric http://black-eric.lj.ru
Дата: 17.12.25 13:52
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.


VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов.


Мы используем
 null!


В принципе этого достаточно. Блок init не будет путать разработчиков?
https://github.com/BlackEric001
Re: Идея для новой версии C#
От: amironov79  
Дата: 17.12.25 14:56
Оценка: 5 (1)
Здравствуйте, VladD2, Вы писали:

VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.


VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку:

VD>
VD>var obj1 = new SomeType1() { Prop1 = null! };
VD>var obj2 = new SomeType2() { Prop1 = obj1 };

VD>obj1.Prop1 = obj2;
VD>


А с помощью анализатора не получится эту задачу решить? Например, Nunit.Analyzers умеет гасить "CS8618: Non-nullable field must contain a non-null value" если инициализация происходит не в конструкторе, а в SetUp методе.
Re[2]: Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.12.25 20:21
Оценка:
Здравствуйте, amironov79, Вы писали:

A>А с помощью анализатора не получится эту задачу решить? Например, Nunit.Analyzers умеет гасить "CS8618: Non-nullable field must contain a non-null value" если инициализация происходит не в конструкторе, а в SetUp методе.


Нет. Язык требует, чтобы все not null поля были заданы при создании. В этом и заключается косяк.

Сам анализатор то сделать можно, но без модификации языка он будет бесполезен.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 17.12.25 20:24
Оценка:
Здравствуйте, BlackEric, Вы писали:

BE>Мы используем

BE>
BE> null!
BE>


Это понятно. Но гордиться тем, что ходишь на костылях — так себе.

BE>В принципе этого достаточно.


Этого ни разу не достаточно. null! тупо отключает проверку, а не позволяет решить проблему.

BE>Блок init не будет путать разработчиков?


Чем? Семантика вполне себе понятная. Мы просто делаем блок в рамках которого можно нарушать правила, а за пределами которого они будут проверены.

Получается что-то типа внешнего конструктора гарантирующего, что за его пределами все ссылки установлены и их нулабельность проверена и не содержит ошибки.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Идея для новой версии C#
От: Sinclair Россия https://github.com/evilguest/
Дата: 18.12.25 09:47
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Компилятор осуществляет проверки нулабельности только при выходе из блока (в конце или по return).


VD>Предполагается, что можно объединять несколько инициализаций:

VD>
VD>init (var obj1 = new SomeType1())
VD>init (var obj2 = new SomeType1())
VD>init (var obj3 = new SomeType1())
VD>{
VD>    obj1.Prop1 = obj2;
VD>    obj2.Prop1 = obj3;
VD>    obj3.Prop1 = obj1;
VD>}
VD>


VD>Так же предполагается, что синтаксис аналогичен using, т.е. если не указаны круглые скобки, блоком является вложенная область видимости.

VD>
VD>void Foo()
VD>{
VD>    init var obj1 = new SomeType1();
VD>    init var obj2 = new SomeType1();
VD>    init var obj3 = new SomeType1();
VD>    obj1.Prop1 = obj2;
VD>    obj2.Prop1 = obj3;
VD>    obj3.Prop1 = obj1;
VD>}
VD>


Идея интересная.
Альтернативная идея — считать variable продекларированной перед блоком инициализации:
var foo = new Foo() { Bar = new Bar { Foo = foo } };

Сейчас так нельзя, потому что foo якобы ещё не существует в точке применения. Но на самом деле к моменту конструирования Bar foo уже сконструирован.
Правда, это тоже не вполне безопасная штука — сеттер Bar.Foo может захотеть положиться на то, что его аргумент полностью инициализирован. В частности, что его non-nullable свойства уже установлены в non-null

В этом плане твоя идея лучше.
Наверное, для её реализации хватит существующих механик definite assignment. Сейчас они применяются к non-nullable полям/свойствам внутри конструктора, плюс отложенная инициализация через инит-блоки (которые декларативные, поэтому там не надо отслеживать все пути через CFG). В твоём варианте нужно убедиться, что присваивания not-null значений во все свойства такого "temporary nullable" объекта доминируют над всеми выходами из блока.

Закидывай им через гитхаб. Мотивация — в том, что присваивание null! не помогает для init-only свойств.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 18.12.25 12:33
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Альтернативная идея — считать variable продекларированной перед блоком инициализации:

S>
S>var foo = new Foo() { Bar = new Bar { Foo = foo } };
S>


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

S>Сейчас так нельзя, потому что foo якобы ещё не существует в точке применения.


Ну так объект реально еще не существует. Переменная какое-то время будет null. Такой подход сработает только в рамках какого-то блока в рамках которого опять же лабильность не должна проверяться.

Я же и предлагаю ввести такой блок, в рамках которого не только нулабельность, но и все правила инициализации (requre, init) как бы отключаются, а за рамками этого блока снова включаются и проверяются. У нас просто есть место в коде где мы как хотим инициализируем объект меняя его свойства, а за пределами мы получаем готовый граф с гарантиями нулабельности и неизменяемости.

S>Но на самом деле к моменту конструирования Bar foo уже сконструирован.


Что считать сконструированностью объекта — это философский вопрос. Вот для объекта графа — это означает, что все нужные поля заполнены и есть общий набор объектов с взаимными ссылками образующих этот самый граф. Для языка — это выполнение ограничений: нулабельность, readonly, requre и init. Вопрос лишь в том, когда их проверять?

S>Правда, это тоже не вполне безопасная штука — сеттер Bar.Foo может захотеть положиться на то, что его аргумент полностью инициализирован. В частности, что его non-nullable свойства уже установлены в non-null


Ну вот я и предлагаю ввести блок, в котором гарантий нет и какой-то код рассчитывающий на то, что все объекты правильно проинициализированы недопустим, но зато можно менять поля и свойства даже если они readonly, requre, init и т.п. Компилятор проверяет, что нет обращений к еще не заполненным полям и позволяет обойти ограничения, который сам же вводит. А за пределами блока проверяются все эти условия (readonly, requre, init) и в случае их нарушений компилятор выдает ошибку. А если ее все пучком, за пределами блока, в рантайме мы получаем готовый граф с объектами удовлетворяющими всем ограничениям. При этом систему типов не придется нарушать разными null!.

S>Наверное, для её реализации хватит существующих механик definite assignment. Сейчас они применяются к non-nullable полям/свойствам внутри конструктора, плюс отложенная инициализация через инит-блоки (которые декларативные, поэтому там не надо отслеживать все пути через CFG). В твоём варианте нужно убедиться, что присваивания not-null значений во все свойства такого "temporary nullable" объекта доминируют над всеми выходами из блока.


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

Сейчас всё тоже самое можно сделать вот этим хаком — null!. Но у тебя нет никаких компайлтайм-гарантий. Ты совершенно спокойно можешь получить null в не налабл-своействе, что делает всю систему дырявой.

S>Закидывай им через гитхаб. Мотивация — в том, что присваивание null! не помогает для init-only свойств.


Да вообще ни для чего не помогает. Это хак, позволяющий обойти ограничения анализа. А с этой фичей можно будет без null! обойтись. Ну по крайней мере в большинстве случаев.

Надо только как-то грамотно это дело продумать перед тем как предложение кидать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Идея для новой версии C#
От: IT Россия linq2db.com
Дата: 18.12.25 15:41
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.


required не подходит?
Если нам не помогут, то мы тоже никого не пощадим.
Re[2]: Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 18.12.25 21:20
Оценка:
Здравствуйте, IT, Вы писали:

IT>required не подходит?


Ты тему то прочел? Оно подходит, но это ж граф! Там циклические ссылки. Вот в моем примере у обоих полей должно быть required, но так как они взаимно рекурсивные, без null! никак не обойтись. А с ним ты просто отключил всю поддержку и надежда есть только на себя.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Идея для новой версии C#
От: _FRED_ Черногория
Дата: 23.12.25 12:14
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.

VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку:
VD>var obj1 = new SomeType1() { Prop1 = null! };
VD>var obj2 = new SomeType2() { Prop1 = obj1 };

VD>obj1.Prop1 = obj2;


Можешь показать, как Prop1 и Prop2 объявлены? По идее уже в их объявлении у тебя может быть засада с
public SomeType2 Prop { get; set; } = null!;


VD>Вот собственно стало очевидно, что система нулабельности для этого не приспособлена.

VD>Предлагаю расширить C# блоком инициализации, внутри которого не будет проверяться нулабельность, а все проверки будут осуществляться при выходе из него:
VD>init (var obj1 = new SomeType1()) // не ругается на то, что obj1.Prop1 не заполнен
VD>…


А #nullable disable и #nullable restore (здесь) не подходят?
#nullable disable

var obj1 = new SomeType1 { Prop = null, }; // No warning here
var obj2 = new SomeType2 { Prop = obj1, };

obj1.Prop = obj2;

#nullable restore

obj1.Prop = GetNullable(); // CS8601 Possible null reference assignment.

static SomeType2? GetNullable() => null;

file sealed class SomeType1 { public SomeType2 Prop { get; set; } = null!; };
file sealed class SomeType2 { public SomeType1 Prop { get; set; } = null!; };


По идее вся засада в том, как объявить подобное свойство, которое должно быть проинициализированно после конструктора / инициализатора?

Я бы попробовал отказаться от null-ов как-то так:
file sealed class SomeType1
{
  public SomeType1() { }

  private SomeType1(bool uninitialized) {
    Debug.Assert(uninitialized);
  }

  internal static SomeType1 Uninitialized { get; } = new SomeType1(uninitialized: true);

  public SomeType2 Prop { get; set; } = SomeType2.Uninitialized;

  public void PostInitialization() {
    if(Prop == SomeType2.Uninitialized) {
      throw new InvalidOperationException();
    }//if
  }
};

file sealed class SomeType2
{
  public SomeType2() { }

  private SomeType2(bool uninitialized) {
    Debug.Assert(uninitialized);
  }

  internal static SomeType2 Uninitialized { get; } = new SomeType2(uninitialized: true);

  public SomeType1 Prop { get; set; } = SomeType1.Uninitialized;

  public void PostInitialization() {
    if(Prop == SomeType1.Uninitialized) {
      throw new InvalidOperationException();
    }//if
  }
};
Help will always be given at Hogwarts to those who ask for it.
Отредактировано 23.12.2025 12:24 _FRED_ . Предыдущая версия .
Re[2]: Идея для новой версии C#
От: VladD2 Российская Империя www.nemerle.org
Дата: 23.12.25 12:57
Оценка:
Здравствуйте, _FRED_, Вы писали:

_FR>Можешь показать, как Prop1 и Prop2 объявлены?


Как requred и not null.

По идее уже в их объявлении у тебя может быть засада с
_FR>
_FR>public SomeType2 Prop { get; set; } = null!;
_FR>


Так это все равно, что полностью контроль отключить. В этом нет ни малейшего смысла.

_FR>По идее вся засада в том, как объявить подобное свойство, которое должно быть проинициализированно после конструктора / инициализатора?


Нет. Засада в том, что поддержка нулабельности в C# сделана убого, на отвяжись. Проинициализировать рекурсивные структуры с ней без её обхода невзможно.

_FR>Я бы попробовал отказаться от null-ов как-то так:


Это всё какая-то фигня переносящая проверки в рантайм. Я же предлагаю устранить проблему в языке, чтобы был полный контроль во время компиляции.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Идея для новой версии C#
От: bobby23  
Дата: 24.12.25 08:48
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.


VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку:

VD>
VD>var obj1 = new SomeType1() { Prop1 = null! };
VD>var obj2 = new SomeType2() { Prop1 = obj1 };

VD>obj1.Prop1 = obj2;
VD>



VD>
VD>var obj1 = new SomeType1() { Prop1 = late_set(obj2) };
VD>var obj2 = new SomeType2() { Prop1 = obj1 };

VD>
VD>

может сделать типа такого?
Отредактировано 24.12.2025 8:49 bobby23 . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.