Это бритвенный код, но я думаю, что то же самое может произойти практически с любым кодом C# в архитектуре, управляемой событиями.
private List<User> Users { get; set; }
protected override async Task OnInitializedAsync()
{
Users = await Context.Users.Include(u => u.Address).ToListAsync();
}
Таким образом, приведенный выше код инициализирует пользователей до того, как к нему будет осуществлен доступ. Однако он выдает предупреждение о том, что переменная, не допускающая значения NULL, не инициализируется.
Является ли это случаем назначения "по умолчанию!" к этому, что я могу сказать, не волнуйтесь, он будет инициализирован до того, как к нему будет доступ?
Обновление: это происходит внутри страницы .razor в части @code. Таким образом, он существует во время рендеринга html для возврата в браузер пользователя. Я пишу этот код в приложении ASP.NET Core Blazor.
Проблема здесь в том, что объект Users должен быть доступен для всего кода в файле .razor. Но код загружается асинхронным методом. И этот метод вызывается как часть создания файла .razor, поэтому он должен быть там.
«Является ли это случаем присвоения ему «по умолчанию!», Что является моим способом сказать, не волнуйтесь, он будет инициализирован до того, как к нему будет осуществлен доступ?» - это просто плохой дизайн класса. Инициализация должна происходить внутри конструктора, потому что конструктор существует для обеспечения инвариантов класса (я знаю, что ctor не может быть async: поэтому, если у вас есть асинхронная логика или какой-либо ввод-вывод, то это должна делать фабричная функция, которая затем вызывает ctor после того, как он данные, которые нужны ctor)....
... что заставляет меня задуматься, для чего предназначено ваше свойство private List<User> Users, если оно эфемерно - это для WinForms или другого компонента пользовательского интерфейса? Если это так, то Users должен быть ObservableCollection<UserViewModel> { get; } = new();, а не List<User> { get; set; } (только get гарантирует, что его ссылка никогда не будет заменена; чтобы заполнить его, вы должны очистить и добавить ObservableCollection с UserViewModel (что обернет User с помощью WPF/etc- удобный интерфейс)...
Ах, вы используете Blazor?
@ Дай, да. И я новичок в этом - мой предыдущий опыт, около 5 лет назад, был ASP.NET MVC. Так что новичок в Blazor и новичок в C#, не допускающем значения NULL (я был генеральным директором последние 7 лет, и поэтому все это время не программировал).
В Blazor применяются те же концепции, что и в WPF: ваше свойство Users должно быть только get-только ObservableCollection, которое очищается и заполняется вашим методом OnInitializedAsync — хотя, вероятно, вы захотите перезагрузить список в какой-то момент в будущем?
@ Дай, ты дал мне очень хорошее руководство о том, как к этому подойти. Спасибо. Кроме того, если вы хотите поставить все это в качестве ответа, я с радостью приму это, чтобы дать вам баллы.





Чтобы уточнить мои комментарии, слишком много понтификаций:
В ООП, независимо от языка (будь то Java, C#, C++, Swift и т. д.), назначение конструктора class — инициализировать состояние экземпляра объекта из любых параметров, а также установить инварианты класса.
Принцип инвариантов классов чаще всего (на мой взгляд) теряется для очень многих (возможно, даже для большинства?) пользователей C#, в частности, из-за того, как язык C# (и связанная с ним документация и библиотеки) эволюционировал в сторону предпочтения конструкторов по умолчанию (без параметров), инициализации после построения и изменяемых свойств - все это не может быть согласовано с теоретическими, а не практическими ограничениями статического анализа, которые компилятор C# использует для #nullable предупреждений.
class может гарантировать состояние любого из своих полей экземпляра (включая автосвойства), тогда как (за исключением членов required в C# 11) инициализатор объекта C# оценивается после создания, является совершенно необязательным, устанавливает полностью произвольные и неисчерпывающие элементы.Поэтому, чтобы использовать #nullable в полной мере, нужно просто привыкнуть к написанию параметризованных конструкторов, несмотря на их «выразительную избыточность» в большинстве случаев (например, в POCO DTO), которые должны повторяться члены класса в качестве параметров ctor, а затем назначьте каждое свойство в ctor.
record types in C# 9 simplify this - but record types aren't as immutable as they seem: properties can still be overwritten with invalid values after the ctor has run in an object-initializer, which breaks the concept of class-invariants being enforced by the constructor - I'm really not happy with how that turned out in C# 9, grrr.Я понимаю, что во многих случаях это невозможно, например, с Entity Framework и EF Core, которые (по состоянию на начало 2023 года) по-прежнему не поддерживают привязку результатов запросов к базе данных к параметрам конструктора, а только к задатчикам свойств, но люди часто не знают, что многие другие библиотеки/фреймворки поддерживают привязку ctor, например, Newtonsoft.Json поддерживает десериализацию объектов JSON в неизменяемые объекты C# через [JsonConstructor] и присоединение [JsonProperty] к каждому параметру ctor, например.
В других случаях, а именно в коде UI/UX, где ваш визуальный компонент/элемент управления/виджет должен наследоваться от некоторого базового класса, предоставленного фреймворком (например, WinForms' System.Windows.Forms.Control, или WPF Visual или Control, или в Blazor: Microsoft.AspNetCore.Components.ComponentBase) - вы находите себя с, казалось бы, противоречивыми предписаниями: вы можете только «инициализировать» состояние/данные элемента управления в методе OnLoad/OnInitializedAsync (а не в конструкторе), но анализ #nullable компилятора C# признает, что только конструктор может инициализировать члены класса и правильно установить, что определенные поля никогда не будут null в любой момент. Это загадка, и документация, официальные примеры и учебные пособия часто замалчивают это (иногда даже с = null!, , что я считаю просто неправильным ).
Возьмем, например, Blazor ComponentBase (поскольку это то, на что, в конце концов, нацелен код OP): сразу после создания подкласса ComponentBase (т.е. ctor запускается) вызывается метод SetParametersAsync, только после этого OnInitializedAsync вызванный - и OnInitializedAsync может завершиться ошибкой или его нужно await изменить, а это означает, что «законы» о конструкторах определенно все еще применяются: любой код, использующий тип ComponentBase, не может обязательно зависеть от какой-либо поздней инициализации с помощью OnInitializedAsync, чтобы быть гарантированным, особенно от любой ошибки- код обработки.
List<User> Users { get; } неинициализированным (и, следовательно, null, несмотря на то, что тип не является List<User>?) и OnInitializedAsync (что установит для него значение, отличное от null), произойдет сбой, а затем, если этот объект подкласса ComponentBase должен был быть передано какой-либо пользовательской логике обработки ошибок, то сам обработчик ошибок потерпит неудачу, если он (справедливо, но неправильно) предположит, что свойство Users никогда не будет null.
Component-фабричной системы, в которой OnInitialized{Async}-логика могла бы быть перемещена в фабричный метод или ctor с той гарантией «все или ничего», которая нам нужна для программного обеспечения, которое мы можем причина о. Но в любом случае...Итак, учитывая вышеизложенное, существует несколько решений:
INotifyPropertyChanged.
List<T> в качестве типа коллекции для свойства, которое будет использоваться для привязки данных: вы должны использовать ObservableCollection<T >, который решает проблему: ObservableCollection<T> предназначен для инициализации только один раз конструктором (таким образом удовлетворяя проблему никогда-null), и рассчитан на долговременное существование и изменчивость, поэтому совершенно нормально заполнять его в OnInitializedAsync и OnParametersSet{Async}, как работает Blazor.Поэтому, на мой взгляд, вы должны изменить свой код на это:
class UsersListComponent : ComponentBase
{
private Boolean isBusy;
public Boolean IsBusy
{
get { return this.isBusy; }
private set { if ( this.isBusy != value ) { this.isBusy = value; this.RaiseOnNotifyPropertyChanged( nameof(this.IsBusy); } // INPC boilerplate, ugh: e.g. https://stackoverflow.com/questions/65813816/c-sharp-blazor-server-display-live-data-using-inotifypropertychanged
}
public ObservableCollection<User> Users { get; } = new ObservableCollection<User>();
protected override async Task OnInitializedAsync()
{
await this.ReloadUsersAsync(); // Also call `ReloadUsersAsync` in `OnParametersSetAsync` as-appropriate based on your application.
}
private async Task ReloadUsersAsync()
{
this.IsBusy = true;
this.Users.Clear(); // <-- The ObservableCollection will raise its own collection-modified events for you, which will be handled by the UI components data-bound to the exposed Users collection.
try
{
List<User> users = await this.Context.Users
.Include(u => u.Address)
.ToListAsync();
// ObservableCollection<T> doesn't support AddRange for historical reasons that we're now stuck with, ugh: https://github.com/dotnet/runtime/issues/18087#issuecomment-359197102
foreach( User u in users ) this.Users.Add( u );
}
finally
{
this.IsBusy = false;
}
}
}
Обратите внимание, что если загрузка данных в OnInitializeAsync через ReloadUsersAsync завершается сбоем из-за исключения, выдаваемого из Entity Framework (что является распространенным, например, тайм-аут SQL Server, недоступность базы данных и т. д.), то инварианты класса UsersListComponent (т. е. что Users коллекция никогда не бывает null, а свойство всегда предоставляет одну долгоживущую ссылку на объект) всегда остаются истинными, что означает, что любой код может безопасно использовать ваш UsersListComponent без риска неожиданного ужасного NullReferenceException.
(И часть IsBusy просто потому, что это неизбежная вещь для добавления в любой класс с привязкой к данным ViewModel/XAML, который выполняет некоторый ввод-вывод).
Это очень полезно. Объяснение за этим особенно так.
архитектура, управляемая событиями" - уххх... Я думаю, вы неправильно используете этот термин. Вы же знаете, как работают инварианты классов?