英文:
How can I create a non-nullable variable when it's initialized in an event?
问题
private List
protected override async Task OnInitializedAsync()
{
Users = await Context.Users.Include(u => u.Address).ToListAsync();
}
上述代码将在首次访问之前初始化Users。然而,它会出现一个警告,表示一个非可为空变量没有被初始化。
这是否意味着要将"default!"赋给它,这是我的一种方式来表示不要担心,在访问之前它将被初始化?
更新: 这发生在.razor页面的@code部分。因此,它存在于HTML被呈现以传递给用户浏览器的过程中。我是在ASP.NET Core Blazor应用程序中编写这段代码的。
问题在于Users对象需要在.razor文件中的所有代码中都可访问。但是,该代码位于异步方法中。并且此方法作为.razor文件创建的一部分被调用,因此它需要在其中。
英文:
This is razor code but I think the same can happen in most any C# code in an event driven architecture.
private List<User> Users { get; set; }
protected override async Task OnInitializedAsync()
{
Users = await Context.Users.Include(u => u.Address).ToListAsync();
}
So the above code will initialize Users before it is ever accessed. However, it puts up a warning that a non-nullable variable is not being initialized.
Is this a case of assigning "default!" to it which is my way of saying don't worry it'll be initialized before it is accessed?
Update: This occurs inside a .razor page in the @code part. So it exists while the html is being rendered to pass back to the user's browser. I'm writing this code in an ASP.NET Core Blazor app.
The problem here is the Users object needs to be accessible to all the code in the .razor file. But the code is loaded in the async method. And this method is called as part of the .razor file creation, so it needs to be in there.
答案1
得分: 0
以下是翻译好的部分:
- 在面向对象编程(OOP)中,无论使用哪种编程语言(如Java、C#、C ++、Swift等),一个
class
的构造函数的目的是初始化对象实例的状态,同时也要建立类不变式。- 顺便说一下,不要将不变性与不可变性混淆:不可变性只是不变性的一种特殊形式,对于一个类的对象而言,它们是可变的是完全可以接受的。
- 类不变性原则在C#用户中通常被忽视,主要是因为C#语言(以及其相关文档和库)的演进偏向了默认(无参数)构造函数、构造后初始化以及可变属性,这些都无法与C#编译器用于
#nullable
警告的静态分析的理论限制协调一致。- 简而言之,只有一个类的构造函数能够保证其实例字段的状态(包括自动属性) - 而除了C# 11中的
required
成员之外,C#对象初始化在构造后执行,完全是可选的,设置完全是任意的,而且不是详尽无遗的。
- 简而言之,只有一个类的构造函数能够保证其实例字段的状态(包括自动属性) - 而除了C# 11中的
- 因此,要充分利用
#nullable
,你必须简单地养成编写带参数的构造函数的习惯,尽管在大多数情况下(例如在POCO DTO中)这在某种程度上会显得“表达冗余”,需要在构造函数中重复写入类的成员作为构造函数参数,然后为每个属性分配值。- 虽然C# 9中的
record
类型简化了这一过程 - 但record
类型并不像看上去那么不可变:属性仍然可以在构造函数运行后在对象初始化中被重写为无效值,这违反了构造函数强制实施的类不变性的概念 - 我对C# 9中的这种设计并不满意。
- 虽然C# 9中的
- 我理解在许多情况下这是不可能的,例如使用Entity Framework和EF Core,它(截至2023年初)仍然不支持将数据库查询结果绑定到构造函数参数,只支持绑定到属性设置器 - 但人们经常不知道许多其他库/框架支持构造函数绑定,比如Newtonsoft.Json支持通过
[JsonConstructor]
将JSON对象反序列化为不可变的C#对象,并为每个构造函数参数附加[JsonProperty]
。 - 在其他情况下,即UI/UX代码中,您的视觉组件/控件/小部件必须继承自某个由框架提供的基类(例如WinForms的
System.Windows.Forms.Control
,或WPF的Visual
或Control
,或在Blazor中:Microsoft.AspNetCore.Components.ComponentBase
) - 您将发现自己面临着看似矛盾的准则:您只能在OnLoad
/OnInitializedAsync
方法中“初始化”控件的状态/数据(而不是构造函数),但C#编译器的#nullable
分析会认识到只有构造函数才能初始化类成员并正确建立某些字段永远不会为null
。这是一个难题,官方文档和示例通常会忽略这一点。 - 以Blazor的
ComponentBase
为例(因为这是OP的代码所针对的):在ComponentBase
子类创建之后立即运行构造函数后,会调用SetParametersAsync
方法,然后才调用OnInitializedAsync
- 而OnInitializedAsync
可能会失败或需要await
,这意味着构造函数的“规则”仍然适用:任何消费ComponentBase
类型的代码都不能依赖OnInitializedAsync
的晚期初始化,特别是不能依赖错误处理代码。- 例如,如果构造函数将
List<User> Users { get; }
属性初始化为空(因此为null
,尽管类型不是List<User>?
),然后OnInitializedAsync
(它将其设置为非null
值)失败,然后如果将ComponentBase
子类对象传递给某些自定义错误处理逻辑,那么如果该错误处理程序本身错误地假定Users
属性永远不会为null
,则该错误处理程序本身将失败。- 但是,这种复杂的安排本来可以避免,如果Microsoft设计Blazor支持某种
Component
工厂系统,其中OnInitialized{Async}
逻辑可以移入具有我们需要的所有或无的构造方法,以便我们可以理解的软件。
- 但是,这种复杂的安排本来可以避免,如果Microsoft设计Blazor支持某种
- 例如,如果构造函数将
- 因此,根据上述情况,有几种解决方案:
- 如果这是一个完全受你控制的系统(Blazor并不是,所以这些建议不适用于你),那么我建议你重新设计系统,使用工厂方法来替代构造函数后的对象初始化。
- 但由于Blazor在数据绑定方法上与WPF,“XAML”,WinForms等具有某些相似之处,即其根源在于实现
INotifyPropertyChanged
的长寿命可变视图模型,这是最重要的因素:由于视图模型概念,你不应该将List<T>
作为用于数据绑定的属性的集合类型,而应该使用ObservableCollection<T>
,它
英文:
To elaborate on my comments, with too much pontification:
-
In OOP, regardless of language (be it Java, C#, C++, Swift, etc), the purpose of a
class
's constructor is to initialize the object-instance's state from any parameters, and also to establish class invariants.- (Btw, don't equate or confuse invariant with immutability: immutability is just a particular kind of invariant, it's perfectly acceptable for a class' objects to be mutable)
-
The principle of class-invariants is more-often-than-lot (in my opinion) lost on so-many (perhaps even most?) users of C#, specifically, because of how the C# language (and its associated documentation and libraries) has evolved towards preferring default (parameterless) constructors, post-construction initialization, and mutable properties - all of which cannot be reconciled with the CS-theoretical, not practical, limits of static analysis which the C# compiler uses for
#nullable
warnings.- The simple fact is only a
class
's constructor can make guarantees about the state of any of its instance fields (including auto-properties) - whereas (exceptingrequired
members in C# 11), a C# object-initializer is evaluated post-construction, is entirely optional, sets entirely arbitrary and non-exhaustive members.
- The simple fact is only a
-
Therefore, to make use of
#nullable
to the full-extent, one must simply get used to getting into the habit of writing parameterized constructors, despite their "expressive redundancy" in most cases (e.g. in a POCO DTO) having to repeat the class's members as ctor parameters and then assign each property in the ctor.- <sub>Though at least
record
types in C# 9 simplify this - butrecord
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.</sub>
- <sub>Though at least
-
I appreciate that this isn't possible in many cases, such as with Entity Framework and EF Core, which (as of early 2023) still doesn't support binding database query results to constructor parameters, only to property-setters - but people often are unaware that many other libraries/frameworks do support ctor binding, such as Newtonsoft.Json supports deserializing JSON objects to immutable C# objects via
[JsonConstructor]
and attaching[JsonProperty]
to each ctor parameter, for example. -
In other cases, namely UI/UX code, where your visual component/control/widget must inherit from some framework-provided base-class (e.g. WinForms'
System.Windows.Forms.Control
, or WPF'sVisual
orControl
, or in Blazor:Microsoft.AspNetCore.Components.ComponentBase
) - you'll find yourself with seemingly contradictory precepts: you can only "initialize" the Control's state /data in theOnLoad
/OnInitializedAsync
method (not the constructor), but the C# compiler's#nullable
analysis recognizes that only the constructor can initialize class members and properly establish that certain fields will never benull
at any point. It's a conundrum and the documentation and official examples and tutorials do often gloss this over (sometimes even with= null!
, which I feel is just wrong). -
Taking Blazor's
ComponentBase
for example (as this is what the OP's code is targeting, after-all): immediately after when theComponentBase
subclass is created (i.e. the ctor runs), theSetParametersAsync
method is called, only after that then isOnInitializedAsync
called - andOnInitializedAsync
can fail or need to beawait
ed, which means that the "laws" about constructors definitely still apply: any code consuming aComponentBase
type cannot necessarily depend any late initialization byOnInitializedAsync
to be guaranteed, especially not any error-handling code.- For example, if the ctor left the
List<User> Users { get; }
property uninitialized (and thereforenull
, despite the type not beingList<User>?
) andOnInitializedAsync
(which would set it to a non-null
value) were to fail, and then if thatComponentBase
subclass object were to be passed to some custom-error handling logic, then that error-handler itself would fail if it (rightfully, but incorrectly) assumed that theUsers
property would never benull
.- However this convoluted arrangement could have been avoided if Microsoft designed Blazor to support some kind of
Component
-factory system whereby theOnInitialized{Async}
logic could be moved into a factory-method or ctor with that all-or-nothing guarantee that we need for software we can reason about. But anyway...
- However this convoluted arrangement could have been avoided if Microsoft designed Blazor to support some kind of
- For example, if the ctor left the
-
So given the above, there exist a few solutions:
- If this were a system entirely under your control <sub>(which Blazor is not, so this advice doesn't apply to you)</sub> then I would recommend you redesign the system to use factory-methods instead of post-ctor object initialization.
- But as this is Blazor, it shares certain similarities in its databinding approach with WPF, "XAML", WinForms and others: namely its roots in having long-lived mutable view-models implementing
INotifyPropertyChanged
.- And this is the most important factor: because the view-model concept means that you shouldn't have
List<T>
as the collection-type for a property that will be used for data-binding: you're meant to useObservableCollection<T>
, which solves the problem:ObservableCollection<T>
is meant to be initialized only once by the constructor (thus satisfying the never-null
problem), and is designed to be long-lived and mutable, so it's perfectly-fine to populate it inOnInitializedAsync
andOnParametersSet{Async}
, which is how Blazor operates.
- And this is the most important factor: because the view-model concept means that you shouldn't have
Therefore, in my opinion, you should change your code to this:
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;
}
}
}
Notice how, if the data-loading in OnInitializeAsync
, via ReloadUsersAsync
, fails due to an exception being thrown from Entity Framework (which is common, e.g. SQL Server timeout, database down, etc) then the class-invariants of UsersListComponent
(i.e. that the Users
collection is never null
and the property always exposes a single long-lived object-reference) always remain true, which means any code can safely consume your UsersListComponent
without risk of a dreaded unexpected NullReferenceException
.
(And the IsBusy
part is just because it's an inevitable thing to add to any ViewModel/XAML-databound class that performs some IO).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论