closed(C# 참조)

closed(C# 참조)

C# 15부터 클래스에 closed 한정자를 달아 *닫힌 계층(closed hierarchy)*을 선언할 수 있어요. 닫힌 클래스에서 직접 파생되는 하위 타입은 오직 같은 어셈블리 안에서만 만들 수 있죠. 직접 하위 타입의 집합이 고정되어 있으니, 각 직접 하위 타입을 모두 처리하는 switch 식은 닫힌 기본 타입을 빠짐없이 다루게 되어 default 분기를 넣지 않아도 돼요.

// Assembly 1
public closed record class JobStatus;
public record class Queued : JobStatus;
public record class Running(int PercentComplete) : JobStatus;
public record class Completed(TimeSpan Elapsed) : JobStatus;
public record class Failed(string Error) : JobStatus;

// Assembly 2
public record class Paused : JobStatus; // Error: 'JobStatus' is a closed class

같은 어셈블리 제한은 닫힌 클래스의 직접 하위 타입에만 적용돼요. 닫힌 클래스에서 파생된 클래스는 그 자체로 닫히지 않아요 — 직접 closed로 표시해야 닫혀요. 앞 예시의 Failed는 그냥 record라서, 다른 어셈블리에서 파생할 수 있어요:

// Assembly 2
public record class RetryableFailed(string Error, int Attempts) : Failed(Error); // OK: 'Failed' isn't sealed or closed

Failed까지 파생을 막고 싶다면, Failedsealedclosed로 선언하면 돼요.

[!NOTE] closed는 상황별 키워드(contextual keyword)예요. 클래스 선언의 한정자로 쓰일 때만 특별한 의미를 가지죠. 다른 상황에서는 계속 closed를 식별자로 쓸 수 있어요. 한정자로도 유효한 위치에서 closed를 식별자로 써야 한다면, 앞에 @를 붙여서(@closed) 컴파일러가 한정자가 아니라 식별자로 취급하게 하면 돼요.

본문

선언 규칙

closed 한정자는 클래스 한정자예요:

  • closed 클래스는 암시적으로 abstract예요. closedsealed, static, 또는 명시적인 abstract 한정자와 함께 쓸 수는 없어요.
  • 닫힌 클래스의 직접 하위 타입은 닫힌 기본 클래스와 같은 어셈블리이면서 같은 모듈에 선언해야 해요.
  • 닫힌 클래스에서 파생된 클래스는 그 자체로 닫히지 않아요. 파생 클래스도 닫고 싶다면 closed 한정자를 다시 적용해야 해요.

제네릭 클래스가 closed 클래스에서 직접 파생된다면, 파생 클래스의 모든 형식 매개변수를 기본 클래스 지정(base class specification)에 사용해야 해요. 이 규칙은 closed 한정자 자체에 관한 게 아니에요 — *닫힌 구성 형식(closed constructed type)*은 형식 인자가 완전히 지정된 제네릭 타입(예: Tree<int>)을 뜻하고, Tree<T> 같은 *개방 형식(open type)*과 대비돼요. 이 규칙 덕분에 기본 클래스의 각 닫힌 구성 형식이 직접 하위 타입 사이에 정확히 하나씩 대응하게 되어, 컴파일러가 완전성(exhaustiveness)을 추론할 수 있어요.

public closed record class Tree<T>;

public record class Leaf<T>(T Value) : Tree<T>;                       // OK: 'T' appears in the base class
public record class Branch<T>(Tree<T> Left, Tree<T> Right) : Tree<T>; // OK: 'T' appears in the base class
public record class Constant<U>(U Value) : Tree<int> { } // Error: 'U' isn't used in the base class

빠짐없이 처리하는 switch 식

switch 식이 닫힌 클래스의 모든 직접 하위 타입을 처리하면, 컴파일러는 그 switch를 완전한 것으로 보고 비완전성 경고를 생성하지 않아요:

:::code language="csharp" source="./snippets/shared/Closed.cs" id="ExhaustiveSwitch":::

switch의 제어 식이 nullable이면, null이 switch가 처리해야 할 또 하나의 가능한 값이 돼요. JobStatus?에 대한 switch는 null까지 함께 다뤄야 완전해요:

:::code language="csharp" source="./snippets/shared/Closed.cs" id="NullableSwitch":::

null 분기를 빠뜨리면 컴파일러가 null 패턴이 처리되지 않았다고 경고해요. 닫힌 타입이 클래스든 nullable로 승격된(lifted) struct든 같은 규칙이 적용돼요.

컴파일러가 완전성을 어떻게 판단하는지, 닫힌 계층이 제네릭 제약과 접근성과 어떻게 함께 작동하는지는 닫힌 계층 패턴을 참고하세요.

닫힌 타입으로 제약된 형식 매개변수

닫힌 계층 기능 사양은 닫힌 클래스로 제약된 형식 매개변수를 완전성 검사 관점에서 그 닫힌 클래스와 동일하게 취급해요. 즉 제어 값이 그런 형식 매개변수를 갖는 switch 식은, 형식 매개변수가 메서드에 붙든 포함 타입에 붙든, 닫힌 제약의 모든 직접 하위 타입을 처리하면 완전해요:

:::code language="csharp" source="./snippets/shared/Closed.cs" id="TypeParameterConstrained":::

C# 언어 사양

더 자세한 내용은 닫힌 계층 기능 사양을 참고하세요.

더 알아보기