Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #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;
}
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.
VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов.
Мы используем
null!
В принципе этого достаточно. Блок init не будет путать разработчиков?
Здравствуйте, VladD2, Вы писали:
VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.
VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку: VD>
А с помощью анализатора не получится эту задачу решить? Например, Nunit.Analyzers умеет гасить "CS8618: Non-nullable field must contain a non-null value" если инициализация происходит не в конструкторе, а в SetUp методе.
Здравствуйте, amironov79, Вы писали:
A>А с помощью анализатора не получится эту задачу решить? Например, Nunit.Analyzers умеет гасить "CS8618: Non-nullable field must contain a non-null value" если инициализация происходит не в конструкторе, а в SetUp методе.
Нет. Язык требует, чтобы все not null поля были заданы при создании. В этом и заключается косяк.
Сам анализатор то сделать можно, но без модификации языка он будет бесполезен.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, BlackEric, Вы писали:
BE>Мы используем BE>
BE> null!
BE>
Это понятно. Но гордиться тем, что ходишь на костылях — так себе.
BE>В принципе этого достаточно.
Этого ни разу не достаточно. null! тупо отключает проверку, а не позволяет решить проблему.
BE>Блок init не будет путать разработчиков?
Чем? Семантика вполне себе понятная. Мы просто делаем блок в рамках которого можно нарушать правила, а за пределами которого они будут проверены.
Получается что-то типа внешнего конструктора гарантирующего, что за его пределами все ссылки установлены и их нулабельность проверена и не содержит ошибки.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Компилятор осуществляет проверки нулабельности только при выходе из блока (в конце или по return).
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 свойств.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, 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! обойтись. Ну по крайней мере в большинстве случаев.
Надо только как-то грамотно это дело продумать перед тем как предложение кидать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, IT, Вы писали:
IT>required не подходит?
Ты тему то прочел? Оно подходит, но это ж граф! Там циклические ссылки. Вот в моем примере у обоих полей должно быть required, но так как они взаимно рекурсивные, без null! никак не обойтись. А с ним ты просто отключил всю поддержку и надежда есть только на себя.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable. VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку:
Можешь показать, как 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 herevar 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.
Так это все равно, что полностью контроль отключить. В этом нет ни малейшего смысла.
_FR>По идее вся засада в том, как объявить подобное свойство, которое должно быть проинициализированно после конструктора / инициализатора?
Нет. Засада в том, что поддержка нулабельности в C# сделана убого, на отвяжись. Проинициализировать рекурсивные структуры с ней без её обхода невзможно.
_FR>Я бы попробовал отказаться от null-ов как-то так:
Это всё какая-то фигня переносящая проверки в рантайм. Я же предлагаю устранить проблему в языке, чтобы был полный контроль во время компиляции.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Рефачу сейчас библиотеку, в которой создается огромный граф. Перевожу его на поддержку #nullable enable.
VD>И понимаю, что современная поддержка nullable в C# очень неполноценная. Большинство ссылок в законченном графе не нулабельные, но код его построения императивный и в нем просто невозможно без хаков проинициализировать свойства во время создания объектов. Приходится прибегать к вот такому хаку: VD>