已关闭层次结构

支持者问题:https://github.com/dotnet/csharplang/issues/9499

总结

允许声明 closed类。 这可以防止直接派生类在不同的程序集中声明:

// Assembly 1
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

// Assembly 2
public record class Locked : GateState; // ERROR - 'GateState' is a closed class

由于所有派生类都声明在封闭类的程序集中,因此涵盖所有这些类的消耗 switch 表达式可以得出结论为“耗尽”关闭类 -它不需要提供默认情况以避免警告。

// Assembly 3
GateState state = ...;
string description = state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open"
    // No warning about missing cases
}; 

动机

许多类类型不是由任何人扩展的,而是他们的作者,但语言没有办法表达这种意图,更不用说防止它发生。 对于类的使用者,这意味着不会将派生类集视为“耗尽”基类,并且开关表达式需要包含一个全部捕获事例以避免警告。

关闭的类提供了一种方法来指示一组派生类已完成,并允许使用代码依赖该代码,以便在 switch 表达式中实现详尽性。

详细设计

Syntax

允许 closed 作为类的修饰符。 类 closed 是隐式抽象的。 因此,它不能也具有 sealed 修饰 static 符。

在类上closed显式使用abstract修饰符是错误的。

派生自已关闭类的类 本身不会 关闭,除非显式声明为。

同一程序集限制

如果声明 closed 了一个程序集中的类,则从另一个程序集中直接派生该类是错误的:

// Assembly 1
public closed class CC { ... } 
public class CO : CC { ... }     // Ok, same assembly

// Assembly 2
public class C1 : CC { ... }     // Error, 'CC' is closed and in a different assembly
public class C2 : CO { ... }     // Ok, 'CO' is not closed

相同的限制适用于模块。 类型的子类型 closed 必须位于与基类型相同的模块中。

类型参数限制

如果泛型类直接派生自封闭类,则必须在基类规范中使用其所有类型参数:

closed class C<T> { ... }
class D1<U> : C<U> { ... }   // Ok, 'U' is used in base class
class D2<V> : C<V[]> { ... } // Ok, 'V' is used in base class
class D3<W> : C<int> { ... } // Error, 'W' is not used in base class

此规则是确保派生类型存在单个泛型实例化,即“耗尽”给定的封闭基类型的泛型实例化。

注意: 如果我们在某些时候允许封闭接口,则此规则可能不够,因为 a) 类可以实现同一接口的多个泛型实例化,b) 接口类型参数可以是共同的或逆变的。 在这种情况下,我们需要优化规则,以继续确保每个封闭基类型的泛型实例化只有一个给定派生类型的泛型实例化。

开关的详尽性

switch处理封闭类的所有直接后代的表达式将被视为已用尽该类。 这意味着将不再提供一些不详尽的警告:

CC cc = ...;
_ = cc switch
{
    CO co => ...,
    // No warning about non-exhaustive switch
};

另一方面,这也意味着关闭的基类在所有直接后代之后都会发生错误:

_ = cc switch
{
    CO co => ...,
    CC cc => ..., // Error, case cannot be reached
};

注意: 对于封闭基类的某些泛型实例化,可能不存在有效的派生类。 详尽的开关只需要为实际可能的派生类型指定事例。

例如:

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

C<string>例如,没有相应的实例化D2<...>,并且无需在开关中给出任何情况D2<...>

C<string> cs = ...;
_ = cs switch
{
    D1<string> d1 => ...,
    // No need for a 'D2<...>' case - no instantiation corresponds to 'C<string>'
}

无法使用子类型时的详尽性

如果子类型在特定使用网站上无效,由于约束冲突、辅助功能冲突或其他原因,则无法通过子类型耗尽开关。

closed class C;
class D1 : C;
class Container
{
    protected class D2 : C;
}

class Program
{
    int M(C c)
        => c switch
        {
            D1 => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

这也适用于泛型子类型不可 说话时,其适用性可能取决于最终类型参数替换。

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

class Program
{
    int M<X>(C<X> c)
        => c switch
        {
            D1<X> => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

子类型约束不会影响详尽性

该语言不会根据基类型和子类型定义中的类型参数的约束来优化子类型的确定。

closed class C<T>;
class D1<U1> : C<U1>;
class D2<U2> : C<U2> where U2 : struct;

class Program
{
    int M1<X>(C<X> c) where X : class
    {
        // warning: switch is not exhaustive. Pattern 'C<X>' is not handled.
        return c switch
        {
            D1<X> => 1,
        };
    }

    int M2<X>(C<X> c) where X : class
    {
        return c switch
        {
            D1<X> => 1,
            C<X> => 2, // ok
        };
    }
}

例如,上述开关表达式,不精确分析构造D2<X>,以意识到所有可能的X违反约束。U2 因此,它假定某些 D2<X> 可能,并要求用户通过耗尽基类型来处理它。

不存在子类型时的详尽性

当关闭的类没有子类型时,空切换不会被视为详尽无遗。

备注:假定这是普通代码中的“中间状态”。 作者很可能对此方案中声明子类型进行更改。 此行为相当于“quirk”--尽管“正在处理所有 0 个子类型”,但语言仍要求用户处理基类型。

closed class C;

class Program
{
    int M1(C c)
        // warning: switch is not exhaustive.
        => c switch
        {
        };

    int M2(C c)
        => c switch
        {
            C => 1, // ok
        };
}

受封闭类型约束的类型参数的详尽性

约束为封闭类的类型参数同样被视为封闭类,目的是进行详尽检查。

closed class C;
class D1 : C;
class D2 : C;

class Program
{
    int M1<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
        };

    int M2<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
            C => 3, // error: 'C' is subsumed by the previous cases
        };
}

确定已关闭类的子类型

通过闭合类类型的开关的详尽性,通过检查开关是否详尽于输入封闭类类型的 子类型集

关闭类的子类型 S 集按以下方式确定:

  1. 对于给定的封闭类型 C,让我们 C₀ 将其原始定义。
  2. 对于其基类型具有原始定义的C₀每个子类型声明S₀,确定是否存在具有基类型的C构造S
  3. 如果存在此类 S 情况,则包含在 子类型集中

封闭类的接口可转换性

如果所有子类型都密封或具有密封层次结构,则表示封闭类具有密封层次结构。 也就是说,扩展层次结构中的所有类都密封或关闭。

当封闭类具有 密封层次结构时,将引入 接口可转换 性限制。 这可以防止尝试转换到接口类型,这不可能成功。

这种限制与从密封类类型到接口类型的 显式引用转换 相似。 请参阅 §10.3.5 显式引用转换

var c = new C();
var i = (I)c; // error

closed class C { }
sealed class D1 : C { }
sealed class D2 : C { }
interface I { }

通过以递归方式收集实现C的接口集及其子类型,我们确定从显式引用转换CI是否存在。 如果接口集包含且未实现,则显式引用转换从 <a0/> 到 。 (如果 C 实现 I,则可以改用隐式引用转换。

降低

已关闭的类是使用 IsClosedType 特性生成的,以允许使用编译器识别它们。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    public sealed class IsClosedTypeAttribute : Attribute { }
}

阻止来自其他语言/编译器的子级

封闭类不应继承自不支持封闭类的语言。 这可以通过添加到 [CompilerFeatureRequired("ClosedClasses")] 已关闭类的所有构造函数来实现。

// Authoring assembly, built with .NET 10 SDK
closed class C1
{
    public C1() { }
    public C1(int param) { }
}

// Consuming assembly, built with .NET 8 SDK
class C2 : C1
{
    public C2() { } // error: 'C1.C1()' requires compiler feature "ClosedClasses"
    public C2() : base(42) { } // error: 'C1.C1(int)' requires compiler feature "ClosedClasses"
}

元数据“视图”为 C1

[IsClosedType]
class C1
{
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
    [CompilerFeatureRequired("ClosedClasses")]
    public C1(int param) { }
}

请注意,与“必需成员”功能不同,除了 CompilerFeatureRequiredAttribute 之外,不会发出 ObsoleteAttribute。 仅发出后者。

多个 CompilerFeatureRequiredAttributes

在如下所示的方案中,编译器将针对与符号相关的每个必需功能发出单独的 CompilerFeatureRequired函数:

closed class C1
{
    public C() { }
    public required string P { get; set; }
}

// Metadata:
class C1
{
    [Obsolete("Types with required members are not supported in this version of your compiler")]
    [CompilerFeatureRequired("RequiredMembers")]
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
}

缺点

  • 可以将修饰符添加到 closed 现有类,或者从封闭类中添加其他派生类,可以是中断性变更。 在发布封闭类之前,作者需要考虑它与使用者暗示的长期合同。

Alternatives

  • 可以使用属性指定[Closed]封闭类,而不是新的closed修饰符。
  • 允许后代的范围可以进一步缩小到文件(尽管 C# 中没有很多先例)或封闭类的主体内作为嵌套类。
  • 可以将封闭的一组允许后代指定为列表,而不是由声明发生的位置隐含。 这将允许在其他程序集中包含类。

可选功能

  • 还可以允许关闭接口。 规则将非常相似。

开放性问题

N/A