文件本地类型
注意
本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/5529
总结
允许顶级类型声明上的 file
修饰符。 类型仅在声明它的文件中存在。
// File1.cs
namespace NS;
file class Widget
{
}
// File2.cs
namespace NS;
file class Widget // different symbol than the Widget in File1
{
}
// File3.cs
using NS;
var widget = new Widget(); // error: The type or namespace name 'Widget' could not be found.
动机
我们的主要动力来自源生成器。 源生成器的工作原理是将文件添加到用户的编译中。
- 这些文件应该能够包含对编译的其余部分隐藏的实现详细信息,但在声明它们的整个文件中都是可用的。
- 我们希望减少生成器“搜索”类型名称的需求,这些类型名称不会与其他生成器中的声明或代码相冲突。
详细设计
当类型具有 file
修饰符时,它被称为 文件级别 类型。
可及性
file
修饰符未分类为辅助功能修饰符。 任何可访问性修饰符都不能与类型上的 file
结合使用。 file
被视为与无障碍功能独立的概念。 由于文件本地类型无法嵌套,因此只有默认的可访问性 internal
可用于 file
类型。
public file class C1 { } // error
internal file class C2 { } // error
file class C3 { } // ok
命名
实现保证不同文件中具有相同名称的文件本地类型与运行时不同。 元数据中类型的可访问性和名称由具体实现决定。 目的是允许编译器在运行时中采用任何适合该功能的未来访问限制功能。 预计在初始实现中,将使用 internal
可访问性,并使用一个无法形容的生成名称,这取决于声明类型的文件。
查找
我们对 成员查找 部分进行了如下修改(新的文本以 粗体显示):
- 接下来,如果
K
为零,则删除其声明包括类型参数的所有嵌套类型。 如果K
不为零,则删除具有不同类型参数数量的所有成员。 当K
为零时,不会删除具有类型参数的方法,因为类型推理过程(§11.6.3)可能能够推断类型参数。- 接下来,让 F 成为包含成员查找所在表达式的编译单元。 从集中删除所有属于文件本地类型且未在 F F 中声明的成员。
- 下一步,如果可访问的成员集中包含文件本地类型,则删除所有属于非文件本地类型的成员。
言论
这些规则禁止在声明它们的文件之外使用文件局部类型。
这些规则还允许文件本地类型 覆盖 命名空间或非文件本地类型:
// File1.cs
class C
{
public static void M() { }
}
// File2.cs
file class C
{
public static void M() { }
}
class Program
{
static void Main()
{
C.M(); // refers to the 'C' in File2.cs
}
}
请注意,我们不会更新规范的 范围 部分。这是因为,正如规范所指出的:
名称的范围是程序文本区域,在该区域中,可以引用名称所声明的实体,而无需对名称进行限定。
实际上,作用域仅影响非限定名称的查找。 这不是我们要使用的正确概念,因为我们还需要作用于限定名称的搜索。
// File1.cs
namespace NS1
{
file class C
{
public static void M() { }
}
}
namespace NS2
{
class Program
{
public static void M()
{
C.M(); // error: C is not in scope
NS1.C.M(); // ok: C can be accessed through NS1.
}
}
}
// File2.cs
namespace NS1
{
class Program
{
C.M(); // error
NS1.C.M(); // error
}
}
因此,我们不会在类型包含的范围方面指定功能,而是在成员查找中指定其他“筛选规则”。
属性
允许将文件本地类用作属性类型,并可用作文件本地类型和非文件本地类型中的属性,就像属性类型是非文件本地类型一样。 文件本地属性类型的元数据名称仍会经历与其他文件本地类型相同的名称生成策略。 这意味着,通过硬编码字符串名称检测文件本地类型是否存在可能是不切实际的,因为它需要取决于编译器的内部名称生成策略,这可能会随时间而变化。 但是,通过 typeof(MyFileLocalAttribute)
检测是有效的。
using System;
using System.Linq;
file class MyFileLocalAttribute : Attribute { }
[MyFileLocalAttribute]
public class C
{
public static void Main()
{
var attribute = typeof(C).CustomAttributes.Where(attr => attr.AttributeType == typeof(MyFileLocalAttribute)).First();
Console.Write(attribute); // outputs the generated name of the file-local attribute type
}
}
签名中的用法
一般需要防止文件本地类型出现在成员参数、返回和类型参数约束中,其中文件本地类型可能不在成员使用点的范围内。
请注意,允许非文件本地类型实现文件本地接口,类似于类型如何实现不太易访问的接口。 视接口成员中存在的类型而定,这可能会导致以下部分的规则被违反。
仅允许在文件本地类型的成员中使用签名
确保这样做的最简单方法是强制实施文件本地类型只能出现在签名中或作为其他文件本地类型的基类型:
file class FileBase
{
}
public class Derived : FileBase // error
{
private FileBase M2() => new FileBase() // error
}
file class FileDerived : FileBase // ok
{
private FileBase M2() => new FileBase(); // ok
}
请注意,这确实限制了显式实现中的用法,即使此类用法是安全的。 为了简化该功能初始迭代的规则,我们这样做。
file interface I
{
void M(I i);
}
class C : I
{
void I.M(I i) { } // error
}
global using static
在 global using static
指令中使用文件本地类型会导致编译时出错,即
global using static C; // error
file class C
{
public static void M() { }
}
实现/重写
文件本地类型声明可以实现接口、替代虚拟方法等,就像常规类型声明一样。
file struct Widget : IEquatable<Widget>
{
public bool Equals(Widget other) => true;
}