主构造函数
注意
本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/2691
总结
类和结构可以具有参数列表,其基类规范可以具有参数列表。 主构造函数参数在整个类或结构声明范围内,如果它们由函数成员或匿名函数捕获,则它们被适当存储(例如声明的类或结构的不可说的私有字段)。
该提议“重构”了记录上已有的主构造函数,使其具有更普遍的功能,并合成了一些额外的成员。
动机
C# 中的类或结构体能够拥有多个构造函数,这提供了通用性,但这也使声明语法变得更加繁琐,因为必须清晰地分隔构造函数的输入和类的状态。
主构造函数将一个构造函数的参数置于整个类或结构的范围内,以便用于初始化或直接用作对象状态。 折衷之处在于,任何其他构造函数都必须通过主构造函数调用。
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
详细设计
这部分描述了记录和非记录的通用设计,然后详细说明了如何通过在主构造函数中添加一组合成成员来指定记录的现有主构造函数。
语法
对类和结构声明进行了扩充,以允许类型名称上的参数列表、基类上的参数列表以及只包含 ;
的正文:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
注意:这些生产取代了记录中的 record_declaration
和记录结构体中的 record_struct_declaration
,它们都已过时。
如果封闭 class_declaration
不包含 parameter_list
,那么 class_base
包含 argument_list
是错误的。 一个部分类或结构体的最多一个部分类型声明可以提供 parameter_list
。 record
声明的 parameter_list
中的参数必须都是值参数。
请注意,根据此提案,class_body
、struct_body
、interface_body
和 enum_body
都被允许只包括一个 ;
。
具有 parameter_list
的类或结构具有隐式公共构造函数,其签名对应于类型声明的值参数。 这称为类型的 主构造函数,并导致隐式声明的无参数构造函数(如果存在)被忽略。 在类型声明中已经存在具有相同签名的主构造函数和构造函数是错误的。
查找
对简单名称的查找进行了增强,以处理主构造函数参数。 以下摘录中的粗体突出显示了相关更改:
- 否则,对于每个实例类型
T
(§15.3.2),从紧接着的封闭类型声明的实例类型开始,然后从每个封闭类或结构声明的实例类型开始(如有):
- 如果
T
的声明包括主构造函数参数I
,并且引用发生在T
的class_base
的argument_list
中或在T
的字段、属性或事件的初始化器内,则结果是主构造函数参数I
- 否则, 如果
e
为零,并且T
声明包含名称为I
的类型参数,则 simple_name 引用该类型参数。- 否则,如果
T
中I
的成员查找 (§12.5) 与e
类型参数产生了匹配项:
- 如果
T
是立即封闭类或结构类型的实例类型,并且查找标识了一个或多个方法,则结果是具有关联实例表达式this
的方法组。 如果指定了类型参数列表,则用于调用泛型方法(§12.8.10.2)。- 否则,如果
T
是立即封闭类或结构类型的实例类型, 如果查找标识实例成员,并且引用发生在实例构造函数、实例方法或实例访问器(§12.2.1)的 块 中,则结果与表单this.I
的成员访问(§12.8.7)相同。 这只能在e
为零时发生。- 否则,结果与
T.I
或T.I<A₁, ..., Aₑ>
形式的成员访问 (§12.8.7) 相同。- 否则,如果
T
声明包含主构造函数参数I
,则结果是主构造函数参数I
。
第一个新增内容对应于记录上的主构造函数所产生的变化,并确保在初始值设定项和基类参数中的任何相应字段之前找到主构造函数参数。 它还将此规则扩展到静态初始值设定项。 但是,由于记录始终具有与参数同名的实例成员,因此扩展只能导致错误消息中的更改。 非法访问参数与非法访问实例成员。
第二个新增功能允许在类型主体的其他地方找到主构造函数参数,但前提是不被成员所覆盖。
如果引用未在下列某个范围内发生,则引用主构造函数参数是错误的:
- 一个
nameof
参数 - 声明类型的实例字段、属性或事件的初始值设定项(带参数的类型声明主构造函数)。
- 声明类型
class_base
中的argument_list
。 - 声明类型的实例方法(注意不包括实例构造函数)的主体。
- 声明类型的实例访问器的主体。
换句话说,主构造函数参数在整个声明类型主体范围内。 它们在声明类型的字段、属性或事件的初始值设定项中,或在声明类型的 class_base
的 argument_list
中,对声明类型的成员进行遮蔽。 它们在其他任何地方都被声明类型的成员所遮蔽。
因此,在以下声明中:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
字段的初始值设定项 i
引用参数 i
,而属性的主体 I
引用字段 i
。
警告来自基础的成员的遮蔽
如果基类成员遮蔽了主构造函数参数,而该主构造函数参数未通过其构造函数传递给基类型,则编译器将在标识符使用时产生警告。
当 class_base中的一个参数满足以下所有条件时,主构造函数参数被视为通过该参数的构造函数传递给基类型:
- 该参数表示主构造函数参数的隐式或显式标识转换;
- 该参数不是扩展
params
参数的一部分;
语义学
主构造函数导致使用给定参数在封闭类型上生成实例构造函数。 如果 class_base
具有参数列表,则生成的实例构造函数将具有具有相同参数列表的 base
初始值设定项。
类/结构声明中的主要构造函数参数可以声明为 ref
、in
或 out
。 在记录声明的主要构造函数中,声明 ref
或 out
参数仍然是非法的。
类正文中的所有实例成员初始值设定项将成为生成的构造函数中的赋值。
如果从实例成员内部引用主构造函数参数,并且引用不在 nameof
参数内部,则会将其捕获到封闭类型的状态中,以便在构造函数终止后仍可访问该参数。 一种可能的实现策略是通过使用混淆名称的专用字段。 在只读结构中,捕获字段将是只读的。 因此,对只读结构的捕获参数的访问将具有与访问只读字段类似的限制。 对只读成员中捕获的参数的访问将具有与访问同一上下文中的实例字段类似的限制。
对于具有类似 ref 类型的参数,不允许捕获,不允许捕获 ref
、in
或 out
参数。 这类似于在 lambdas 中捕获的限制。
如果主要构造函数参数仅从实例成员初始值设定项内引用,则这些参数可以直接引用生成的构造函数的参数,因为它们作为它的一部分执行。
主构造函数将执行以下操作序列:
- 参数值存储在捕获字段中(如果有)。
- 执行实例初始值设定项
- 调用基本构造函数初始值设定项
任何用户代码中的参数引用都替换为相应的捕获字段引用。
例如,此声明:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
生成类似于以下内容的代码:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
非主构造函数声明与主构造函数具有相同的参数列表是错误的。 所有非主构造函数声明都必须使用 this
初始值设定项,以便最终调用主构造函数。
如果主构造器参数未在(可能生成的)实例初始化器或基初始化器内读取,则记录会生成警告。 对于类和结构中的主构造函数参数,将报告类似的警告。
- 如果该参数未被捕获,也未在任何实例初始值设定项或基础初始值设定项中读取,则该参数将被视为按值传递的参数。
- 如果参数未在任何实例初始值设定项或基础初始值设定项中读取,则该
in
参数将被忽略。 - 如果参数未在任何实例初始值设定项或基础初始值设定项中读取或写入,则该
ref
参数将被忽略。
相同的简单名称和类型名称
对于通常被称为“Color Color”的场景,有一条特殊的语言规则 - 相同的简单名称和类型名称。
在表单
E.I
的成员访问中,如果E
是单个标识符,并且E
作为 simple_name(§12.8.4)的含义是常量、字段、属性、局部变量或参数,其类型与 type_nameE
的含义相同(§7.8.1), 然后,允许E
的两种可能含义。E.I
的成员查找绝不模棱两可,因为在这两种情况下,I
必定是E
类型的成员。 换句话说,规则只是允许访问E
的静态成员和嵌套类型,否则会发生编译时错误。
对于主构造函数,该规则会影响实例成员中的标识符是应被视为类型引用,还是作为主构造函数参数引用,而主构造函数参数引用又将参数捕获到封闭类型的状态。 即使“E.I
的成员查找从不含糊”,但当查找生成成员组时,在某些情况下,在不完全解析(绑定)成员访问的情况下,无法确定成员访问是引用静态成员还是实例成员。 同时,捕获主构造函数参数会以影响语义分析的方式更改封闭类型的属性。 例如,该类型可能会变成非托管,并因此无法满足某些约束条件。
甚至有些情况下,根据参数是否被捕获,两种绑定方式都可能成功。 例如:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
如果我们将接收器 Color
视为一个值,就可以捕捉到参数,“S1”就变成了托管。 然后,静态方法由于约束而变得不可应用,我们将调用实例方法。 但是,如果将接收方视为类型,我们不会捕获参数,并且“S1”保持不变,则这两种方法都适用,但静态方法“更好”,因为它没有可选参数。 这两种选择都不会导致错误,但每个选项都会导致不同的行为。
鉴于此情况,当满足以下所有条件时,编译器将为成员访问 E.I
生成歧义错误:
- 对
E.I
进行成员查找会产生一个同时包含实例和静态成员的成员组。 适用于接收方类型的扩展方法被视为用于此检查的实例方法。 - 如果
E
被视为简单名称,而不是类型名称,它将引用主构造函数参数,并将参数捕获到封闭类型的状态。
双重存储警告
如果将主构造函数参数传递给基类并同时捕获,那么该参数就很有可能无意中在对象中存储了两次。
当以下条件全部为 true 时,编译器将对 in
或 class_base
argument_list
中的参数值产生警告:
- 该参数表示主构造函数参数的隐式或显式标识转换;
- 该参数不是扩展
params
参数的一部分; - 主构造函数参数被捕获到封闭类型的状态中。
如果满足以下所有条件,编译器将为 variable_initializer
生成警告:
- 变量初始值设定项表示主构造函数参数的隐式或显式身份转换。
- 主构造函数参数被捕获到封闭类型的状态中。
例如:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
针对主构造函数的属性
在 https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md,我们决定接受 https://github.com/dotnet/csharplang/issues/7047 提议。
允许在带有 parameter_list 的 class_declaration/struct_declaration 中使用“method”属性目标,并导致相应的主构造函数具有该属性。
在没有 parameter_list 的 class_declaration/struct_declaration 中,目标为 method
的属性会被忽略并发出警告。
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
记录的主构造函数
通过此建议,记录不再需要单独指定主构造函数机制。 相反,具有主构造函数的记录(类和结构体)声明将遵循一般规则,并添加以下简单的内容:
- 对于每个主构造函数参数,如果已存在具有相同名称的成员,则它必须是实例属性或字段。 否则,将会合成一个同名的公共仅初始化的自动属性,该属性初始值设定项将从参数赋值。
- 析构函数使用 out 参数合成,以便与主构造函数参数匹配。
- 如果显式构造函数声明是“复制构造函数”(采用封闭类型的单个参数的构造函数)则不需要调用
this
初始值设定项,并且不会执行记录声明中存在的成员初始值设定项。
缺点
- 构造对象的分配大小不太明显,因为编译器根据类的全文确定是否为主构造函数参数分配字段。 此风险类似于对 lambda 表达式中变量的隐式捕获。
- 在多个继承级别中重复捕获“相同”参数是一个常见的诱惑(或意外模式),因为参数沿构造函数链传递时,并没有在基类中显式分配受保护的字段,从而导致对象中相同数据的重复分配。 这与当前用自动属性重写自动属性的风险非常相似。
- 按照这里的提议,构造函数主体中通常会有一些额外的逻辑,但这里却没有这些逻辑的位置。 下面的“主构造函数机构”扩展就解决了这一问题。
- 如建议的那样,执行顺序语义与普通构造函数内部略有不同,将成员初始值设定项延迟到基本调用之后。 这一点也许可以弥补,但要以某些扩展提议(特别是“主构造函数主体”)为代价。
- 该建议仅适用于可以将单个构造函数指定为主要构造函数的场景。
- 无法表达类和主构造函数的单独可访问性。 例如,当公共构造函数都委托给一个专用的“万能”构造函数时。 如果有必要,以后可以提出语法建议。
替代方案
无捕获
该功能的另一个简单版本是禁止在成员主体中出现主构造函数参数。 引用它们将是一个错误。 如果需要超出初始化代码的存储,则必须显式声明字段。
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
这仍可在以后发展为完整的提议,并可避免一些决定和复杂性,但代价是最初删除的模板较少,而且可能也显得不直观。
显式生成的字段
另一种方法是让主构造函数参数始终且明显地生成同名字段。 不会以与本地和匿名函数相同的方式封闭参数,而是会显式地生成成员声明,类似于在记录中为主要构造函数参数生成的公共属性。 就像记录一样,如果已存在合适的成员,则不会生成一个。
如果生成的字段是私有的,那么当它不在成员体中用作字段时,仍然可以被省略。 但是,在类中,专用字段通常不是正确的选择,因为该字段在派生类中可能会导致状态重复。 此处的一个选项是改为在类中生成受保护的字段,鼓励在继承层之间重复使用存储。 但是,这样我们就无法省略声明,而且每个主构造函数参数都会产生分配成本。
这将使非记录主构造函数与记录构造函数更加一致,因为成员总是(至少在概念上)被生成,尽管成员类型不同且有着不同的访问权限。 但它也会导致与 C# 中其他位置参数和局部变量的捕获方式产生惊人的差异。 例如,如果我们曾经允许本地类,则它们会隐式捕获封闭参数和局部变量。 明显地为其生成阴影区域似乎不是一种合理的行为。
此方法经常引发的另一个问题是,许多开发人员对参数和字段有不同的命名约定。 应该使用哪一个作为主构造函数参数? 任一选择都会导致代码的其余部分不一致。
最后,显式生成成员声明是记录类的主要特征,但对于非记录类和结构体来说,这就更令人吃惊和“不符合常规”了。 总之,这就是为什么主要提案选择隐式捕获,并在需要时为显式成员声明提供合理行为(与记录一致)的原因。
从初始化器的范围中删除实例成员
上面的查找规则旨在允许在手动声明相应成员时记录中主要构造函数参数的当前行为,并在未声明时解释生成的成员的行为。 这就要求在“初始化范围”(this/base 初始值设定项、成员初始值设定项)和“主体范围”(成员主体)之间进行不同的查找,而上述建议是通过改变当主构造函数参数被查找时(取决于引用发生的位置)来实现这一点的。
一个观察结果是,在初始值设定项范围中使用简单名称引用实例成员总是会导致错误。 我们是否可以干脆将这些地方的实例成员排除在范围之外,而不是仅仅跟踪这些地方的实例成员? 这样,就不会出现这种奇怪的范围条件排序。
这种替代方案可能是可能的,但它会产生一些影响深远和可能不受欢迎的后果。 首先,如果我们将实例成员从初始值设定项范围中移除,那么与实例成员而不是主构造函数参数刚好相对应的简单名称可能会意外绑定到类型声明之外的内容! 这似乎很少是有意为之,出错可能反倒更好。
此外,静态成员可以在初始化范围中引用。 因此,我们必须区分查找中的静态成员和实例成员,这是我们今天不执行的操作。 (我们在解决重载问题时会有所区别,但在这里并不适用)。 因此,这一点也必须改变,从而导致更多的情况,例如在静态上下文中,某些内容会“进一步”绑定,而不是因为找到了实例成员而出错。
总之,这种“简化”会导致下游复杂化,而这会使人感到措手不及。
可能的扩展
这些是核心建议的变更或补充,可以与核心建议同时考虑,或者在稍后阶段如果认为有用的话再考虑。
构造函数中对主构造函数参数的访问
上述规则导致在另一个构造函数中引用主构造函数参数出现错误。 然而,在其他构造函数的主体中也可以这样做,因为主构造函数会首先运行。 不过,在 this
初始值设定项的参数列表中,仍需禁止使用该参数。
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
此类访问仍会导致捕获,因为在主构造函数已经运行之后,构造函数体要访问该变量的唯一方法就是通过捕获。
可以放宽对 this 初始化程序参数中主构造函数参数的禁止,使这些参数可以使用,但不会被明确分配。不过,这似乎没有意义。
允许构造函数不带 this
初始值设定项
可以允许不使用 this
初始值设定项(即具有隐式或显式 base
初始值设定项)的构造函数。 这样的构造函数将不会运行实例字段、属性和事件初始值设定项,因为这些将被视为主构造函数的一部分。
在存在此类基调用构造函数的情况下,有几种选项可用于处理主构造函数参数捕获。 最简单的是完全禁止在这种情况下捕获。 主要构造函数参数仅在此类构造函数存在时才用于初始化。
或者,如果与前面描述的选项结合使用,以允许在构造函数中访问主要构造函数参数,则参数初始进入构造函数主体时可能未明确分配,而被捕获的参数需要在构造函数主体结束时明确分配。 它们本质上是隐式的输出参数。 这样一来,捕获的主要构造函数参数在被其他函数成员使用时,将始终拥有一个合理的(即被明确分配的)值。
这种扩展(无论哪种形式)的吸引力在于,它完全概括了当前对记录中“复制构造函数”的豁免,而不会导致观察到未初始化主构造函数参数的情况。 从本质上讲,以替代方式初始化对象的构造函数是正常的。 与捕获相关的限制不会是记录中现有手动定义的复制构造函数的重大更改,因为记录永远不会捕获其主构造函数参数(而是生成字段)。
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
主构造函数主体
构造函数本身通常包含参数验证逻辑或其他无法表示为初始化器的较复杂的初始化代码。
可以扩展主构造函数,以允许语句块直接显示在类体中。 这些语句将被插入到生成的构造函数中,出现在初始化赋值之间,因此将与初始值设定项一起执行。 例如:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
如果我们引入“最终初始值设定项”,在构造函数和任何对象/集合初始值设定项运行完毕后再运行“最终初始值设定项”,那么很多情况都可以得到充分解决。 但是,参数验证是理想情况下尽早发生的一件事。
主构造函数主体还可以为主构造函数提供一个允许访问修改器的位置,允许它偏离封闭类型的可访问性。
组合参数和成员声明
一种可能的、经常被提及的补充方法是允许对主构造函数参数进行批注,使其同时声明类型中的一个成员。 通常建议对参数启用访问说明符以触发成员生成:
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
存在一些问题:
- 如果需要属性而不是字段,该怎么办? 在参数列表中内联
{ get; set; }
语法似乎并不讨人喜欢。 - 如果将不同的命名约定用于参数和字段,该怎么办? 然后,此功能是毫无用的。
这是未来可能增加的内容,是否采用可以自行决定。 目前的提议保留了这种可能性。
开放性问题
类型参数的查找顺序
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup 节规定,在任何上下文中,声明类型的类型参数都应位于类型主要构造函数的参数之前。 但是,我们已经有了使用记录的行为 - 在基本初始值设定项和字段初始值设定项中,主构造函数参数排在类型参数之前。
我们应该对此差异做些什么?
- 调整规则以匹配行为。
- 调整行为(可能的重大变更)。
- 禁止主构造函数参数使用类型参数名称(可能的重大更改)。
- 不执行任何操作,接受规范与实现之间的不一致。
结论:
调整规则以匹配行为(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors)。
用于捕获主构造函数参数的字段目标属性
是否应该允许为捕获的主构造函数参数设置字段目标属性?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
现在,无论是否捕获参数,属性都会被忽略并显示警告。
请注意,对于记录而言,在为其合成属性时,允许使用字段目标属性。 然后,属性就会出现在后备字段中。
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
结论:
警告来自基础的成员的遮蔽
在基类的某个成员在成员内部遮蔽主构造函数参数时(请参阅 https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621),我们是否应该报告警告?
结论:
另一种设计获得批准 - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
在闭包中捕获封闭类型的实例
当捕获到外层类型状态中的参数也在实例初始值设定项或基本初始值设定项内的 lambda 中引用时,lambda 和外层类型状态应引用参数的相同位置。 例如:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
由于将参数捕获到类型状态的本机实现只是在专用实例字段中捕获参数,因此 lambda 需要引用同一字段。 因此,它需要能够访问类型的实例。 这需要在调用基类构造函数之前将 this
捕获到闭包中。 这反过来又产生了安全但无法验证的 IL。 这是可以接受的吗?
或者,我们可以:
- 禁止使用这类 lambda 表达式;
- 或者,也可以在一个单独的类实例(另一个闭包)中捕获参数,并在闭包和封闭类型的实例之间共享该实例。 这样,就不需要在闭包中捕获
this
了。
结论:
在调用基构造函数(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md)之前,我们能够轻松地将 this
捕获到闭包中。
运行时团队也没有发现 IL 模式有问题。
为结构体中的 this
赋值
C# 允许在结构中分配 this
。 如果结构捕获主构造函数参数,则赋值将覆盖其值,这可能对用户不明显。 这样的赋值是否要发出警告?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
结论:
允许,无警告(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md)。
初始化和捕获的双重存储警告
如果构造函数参数被传递到基类,并且 也被 捕获,则会有警告,因为参数很可能会在对象中被无意地存储两次。
似乎,如果一个参数用于初始化一个成员且也被捕获,那么会存在类似的风险。 下面是一个小示例:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
对于给定的 Person
实例,对 Name
所做的更改不会反映在 ToString
的输出中,这可能不是开发人员的本意。
我们是否应对这种情况引入双重存储警告?
这是其工作原理:
如果满足以下所有条件,编译器将为 variable_initializer
生成警告:
- 变量初始值设定项表示主构造函数参数的隐式或显式标识转换;
- 主构造函数参数被捕获到封闭类型的状态中。
结论:
已批准,请参阅https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
LDM 会议
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors