다음을 통해 공유


15개 클래스

15.1 일반

클래스는 데이터 멤버(상수 및 필드), 함수 멤버(메서드, 속성, 이벤트, 인덱서, 연산자, 인스턴스 생성자, 종료자 및 정적 생성자) 및 중첩된 형식을 포함할 수 있는 데이터 구조입니다. 클래스 형식은 파생 클래스가 기본 클래스 확장하고 특수화할 수 있는 메커니즘인 상속을 지원합니다.

15.2 클래스 선언

15.2.1 일반

class_declaration 새 클래스를 선언하는 type_declaration(§14.7)입니다.

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

class_declaration은 선택적 특성의 집합(§22), 이어서 선택적 class_modifier의 집합(§15.2.2), 선택적 partial 한정자(§15.2.7), 이어서 class 키워드 및 클래스를 명명하는 식별자로 구성됩니다. 이어서 선택적 type_parameter_list(§15.2.3), 이어서 선택적 class_base 명세(§15.2.4), 이어서 선택적 type_parameter_constraints_clause의 집합(§15.2.5), 이어서 class_body(§15.2.6)가 뒤따르고, 이어서 선택적으로 세미콜론이 올 수 있습니다.

클래스 선언은 type_parameter_list제공하지 않는 한 type_parameter_constraints_clause제공하지 않습니다.

type_parameter_list 제공하는 클래스 선언은 제네릭 클래스 선언입니다. 또한 제네릭 클래스 선언 또는 제네릭 구조체 선언 내에 중첩된 모든 클래스는 제네릭 클래스 선언 자체이며, 포함된 형식에 대한 형식 인수는 생성된 형식(§8.4)을 만들기 위해 제공되어야 하기 때문에 제네릭 클래스 선언입니다.

15.2.2 클래스 한정자

15.2.2.1 일반

class_declaration 필요에 따라 클래스 한정자 시퀀스를 포함할 수 있습니다.

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

동일한 한정자가 클래스 선언에 여러 번 표시되는 것은 컴파일 시간 오류입니다.

new 수식자는 중첩 클래스에서 허용됩니다. 이 클래스는 §15.3.5설명된 대로 동일한 이름으로 상속된 멤버를 숨기게 지정합니다. 클래스 선언이 중첩 클래스 선언이 아닌 경우, 한정자 new가 나타나는 것은 컴파일 시간 오류입니다.

public, protected, internalprivate 한정자는 클래스의 접근성을 제어합니다. 클래스 선언이 발생하는 컨텍스트에 따라 이러한 한정자 중 일부는 허용되지 않을 수 있습니다(§7.5.2).

부분 형식 선언(§15.2.7)에 접근성 사양(, public, protectedinternal 한정자를 통해private)이 포함된 경우 해당 사양은 접근성 사양을 포함하는 다른 모든 부분에 동의해야 합니다. 부분 형식의 일부가 접근성 사양을 포함하지 않는 경우 형식에 적절한 기본 접근성(§7.5.2)이 제공됩니다.

abstract, sealedstatic 한정자는 다음 하위클래스에 설명되어 있습니다.

15.2.2.2 추상 클래스

abstract 한정자는 클래스가 불완전하고 기본 클래스로만 사용되도록 함을 나타내는 데 사용됩니다. 추상 클래스다음과 같은 방법으로 비 추상 클래스와 다릅니다.

  • 추상 클래스는 직접 인스턴스화할 수 없으며 추상 클래스에서 연산자를 사용하는 new 것은 컴파일 시간 오류입니다. 컴파일 시간 형식이 추상인 변수와 값을 가질 수 있지만 이러한 변수 및 값은 추상 형식에서 파생된 비 추상 클래스의 인스턴스에 대한 참조이거나 반드시 포함됩니다 null .
  • 추상 클래스는 추상 멤버를 포함할 수 있지만 필수는 아닙니다.
  • 추상 클래스는 봉인할 수 없습니다.

비 추상 클래스가 추상 클래스에서 파생되는 경우 비 추상 클래스는 상속된 모든 추상 멤버의 실제 구현을 포함하므로 이러한 추상 멤버를 재정의해야 합니다.

예제: 다음 코드에서

abstract class A
{
    public abstract void F();
}

abstract class B : A
{
    public void G() {}
}

class C : B
{
    public override void F()
    {
        // Actual implementation of F
    }
}

추상 클래스 A 는 추상 메서드 F를 소개합니다. 클래스 B 는 추가 메서드G를 도입하지만 구현 FB 을 제공하지 않으므로 추상으로 선언되어야 합니다. 클래스 CF 을 오버라이드하고 실제 구현을 제공합니다. 추상 멤버 CC 가 없으므로 추상이 아닌 것으로 허용됩니다(필수는 아님).

끝 예제

클래스의 부분 형식 선언(§15.2.7)에 있는 하나 이상의 부분이 한정자를 포함하는 abstract 경우 클래스는 추상입니다. 그렇지 않으면 클래스는 추상적이지 않습니다.

15.2.2.3 봉인된 클래스

sealed 수식어는 클래스에서 파생되지 않도록 방지하는 데 사용됩니다. 봉인된 클래스를 다른 클래스의 기본 클래스로 지정하면 컴파일 시간 오류가 발생합니다.

봉인된 클래스는 추상 클래스일 수도 없습니다.

참고: sealed 수정자는 주로 의도하지 않은 파생을 방지하기 위해 사용되지만, 특정 런타임 최적화를 가능하게 합니다. 특히 봉인된 클래스에는 파생 클래스가 없는 것으로 알려져 있으므로 봉인된 클래스 인스턴스의 가상 함수 멤버 호출을 가상이 아닌 호출로 변환할 수 있습니다. 끝 메모

클래스의 부분 형식 선언(§15.2.7)에 한정자가 포함된 하나 이상의 부분이 있으면 sealed 클래스가 봉인됩니다. 그렇지 않으면 클래스가 해제됩니다.

15.2.2.4 정적 클래스

15.2.2.4.1 일반

static 정자는 정적 클래스로 선언되는 클래스를 표시하는 데 사용됩니다. 정적 클래스는 인스턴스화되지 않아야 하며 형식으로 사용해서는 안 되며 정적 멤버만 포함해야 합니다. 정적 클래스만 확장 메서드 선언(§15.6.10)을 포함할 수 있습니다.

정적 클래스 선언에는 다음과 같은 제한 사항이 적용됩니다.

  • 정적 클래스에는 sealed 또는 abstract 수식어를 포함해서는 안 됩니다. 그러나 정적 클래스는 인스턴스화하거나 파생할 수 없으므로 봉인된 클래스와 추상 클래스 모두인 것처럼 동작합니다.
  • 정적 클래스는 class_base 사양(§15.2.4)을 포함하지 않으며 기본 클래스 또는 구현된 인터페이스 목록을 명시적으로 지정할 수 없습니다. 정적 클래스는 암시적으로 형식 object에서 상속됩니다.
  • 정적 클래스는 정적 멤버(§15.3.8)만 포함해야 합니다.

    참고: 모든 상수 및 중첩된 형식은 정적 멤버로 분류됩니다. 끝 메모

  • 정적 클래스에는 protected, private protected, 또는 protected internal 접근성이 선언된 멤버가 없어야 합니다.

이러한 제한 사항을 위반하는 것은 컴파일 시간 오류입니다.

정적 클래스에는 인스턴스 생성자가 없습니다. 정적 클래스에서 인스턴스 생성자를 선언할 수 없으며 정적 클래스에 대해 기본 인스턴스 생성자(§15.11.5)가 제공되지 않습니다.

정적 클래스의 멤버는 자동으로 정적이지 않으며 멤버 선언에는 상수 및 중첩 형식을 제외하고 한정자가 명시적으로 포함되어 static 야 합니다. 클래스가 정적 외부 클래스 내에 중첩된 경우 한정자를 명시적으로 포함하지 않는 한 중첩 클래스는 정적 클래스가 static 아닙니다.

클래스의 부분 형식 선언(§15.2.7)에 있는 하나 이상의 부분이 한정자를 포함하는 static 경우 클래스는 정적입니다. 그렇지 않으면 클래스가 정적이지 않습니다.

15.2.2.4.2 정적 클래스 형식 참조

정적 클래스를 참조할 수 있는 경우 namespace_or_type_name(§7.8)가 허용됩니다.

  • namespace_or_type_nameT 형식의 namespace_or_type_name에서 T.I입니다 또는
  • namespace_or_type-nameT(§12.8.18)의 형식typeof(T)에 있습니다.

정적 클래스를 참조할 수 있는 경우 primary_expression(§12.8)가 허용됩니다.

  • primary_expressionE(§12.8.7)의 형식에서 E.I입니다.

다른 컨텍스트에서 정적 클래스를 참조하는 것은 컴파일 시간 오류입니다.

참고: 예를 들어 정적 클래스를 기본 클래스, 멤버의 구성 요소 형식(§15.3.7), 제네릭 형식 인수 또는 형식 매개 변수 제약 조건으로 사용하는 것은 오류입니다. 마찬가지로 배열 형식, 새 식, 캐스트 식, is 식, as 식, sizeof 식 또는 기본값 식에서 정적 클래스를 사용할 수 없습니다. 끝 메모

15.2.3 형식 매개 변수

형식 매개 변수는 생성된 형식을 만들기 위해 제공된 형식 인수의 자리 표시자를 나타내는 간단한 식별자입니다. constrast로 형식 인수(§8.4.2)는 생성된 형식을 만들 때 형식 매개 변수로 대체되는 형식입니다.

type_parameter_list
    : '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
    ;

decorated_type_parameter
    : attributes? type_parameter
    ;

type_parameter §8.5정의되어 있습니다.

클래스 선언의 각 형식 매개 변수는 해당 클래스의 선언 공간(§7.3)에 이름을 정의합니다. 따라서 해당 클래스의 다른 형식 매개 변수 또는 해당 클래스에 선언된 멤버와 같은 이름을 가질 수 없습니다. 형식 매개 변수는 형식 자체와 같은 이름을 가질 수 없습니다.

두 개의 부분 일반 형 구성 선언(같은 프로그램 내에서)이 동일한 완전한 자격 이름을 가질 경우(여기에는 형식 매개변수의 수를 명시하는 generic_dimension_specifier (§12.8.18)가 포함됨), 동일한 비바운드 제네릭 형식에 기여합니다 (§7.8.3). 이러한 두 부분 형식 선언은 각 형식 매개 변수에 대해 동일한 이름을 순서대로 지정해야 합니다.

15.2.4 클래스 기본 사양

15.2.4.1 일반

클래스 선언에는 클래스의 직접 기본 클래스와 클래스에서 직접 구현하는 인터페이스(§18)를 정의하는 class_base 사양이 포함될 수 있습니다.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 기본 클래스

class_typeclass_base에 포함된 경우, 선언되는 클래스의 직접 기본 클래스를 지정합니다. 부분 클래스 선언에 class_base가 없거나, 또는 class_base가 인터페이스 타입만 나열하는 경우, 직접적인 기본 클래스는 로 간주됩니다. partial 클래스 선언에 기본 클래스 사양이 포함된 경우 해당 기본 클래스 사양은 기본 클래스 사양을 포함하는 해당 부분 형식의 다른 모든 부분과 동일한 형식을 참조해야 합니다. 부분 클래스의 일부가 기본 클래스 사양을 포함하지 않는 경우 기본 클래스는 다음과 같습니다 object. 클래스는 §15.3.4에 설명된 대로 직접 기본 클래스에서 멤버를 상속합니다.

예제: 다음 코드에서

class A {}
class B : A {}

클래스 A 는 의 직접 기본 클래스 B라고하며 B 에서 A파생되었다고합니다. A 직접 기본 클래스를 명시적으로 지정하지 않으므로 직접 기본 클래스는 암시적으로 지정됩니다object.

끝 예제

제네릭 형식 선언(§15.3.9.7) 내에 선언된 중첩 형식을 포함하여 생성된 클래스 형식의 경우 제네릭 클래스 선언에서 기본 클래스를 지정하면 생성된 형식의 기본 클래스는 기본 클래스 선언의 각 type_parameter 대해 생성된 형식의 해당 type_argument 대체하여 가져옵니다.

: 제네릭 클래스 선언이 지정된 경우

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

생성된 형식 G<int> 의 기본 클래스는 다음과 입니다 B<string,int[]>.

끝 예제

클래스 선언에 지정된 기본 클래스는 생성된 클래스 형식(§8.4)일 수 있습니다. 기본 클래스는 범위 내의 형식 매개 변수를 포함할 수 있지만 자체적으로 형식 매개 변수(§8.5)가 될 수 없습니다.

예제:

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

끝 예제

클래스 형식의 직접 기본 클래스는 적어도 클래스 형식 자체(§7.5.5)만큼 액세스할 수 있어야 합니다. 예를 들어, public 클래스가 private 또는 internal 클래스에서 상속하려고 하면 컴파일 시간 오류가 발생합니다.

클래스 형식의 직접 기본 클래스는 System.Array, System.Delegate, System.Enum, System.ValueType 혹은 dynamic 형식 중 하나일 수 없습니다. 또한 제네릭 클래스 선언은 System.Attribute를 직접 또는 간접 기본 클래스로 사용하지 않아야 합니다(§22.2.1).

클래스A의 직접 기본 클래스 사양 B 의 의미를 결정할 때의 직접 기본 클래스는 일시적으로 가정B되므로 기본 클래스 object 사양의 의미는 재귀적으로 자체에 의존할 수 없습니다.

: 다음

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

에서 의 직접 기본 클래스가 로 간주되므로(§7.8의 규칙에 따라) 멤버를 가지지 않는 것으로 간주되므로 오류가 발생합니다.

끝 예제

클래스의 기본 클래스는 직접적인 기본 클래스와 그의 기본 클래스들입니다. 즉, 기본 클래스 집합은 직접 기본 클래스 관계의 전이적 닫기입니다.

: 다음에서:

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

의 기본 클래스는 D<int>C<int[]>, B<IComparable<int[]>>Aobject.

끝 예제

클래스 object를 제외하고 모든 클래스에는 정확히 하나의 직접 기본 클래스가 있습니다. 클래스에는 object 직접 기본 클래스가 없으며 다른 모든 클래스의 궁극적인 기본 클래스입니다.

클래스가 자신에게 의존하는 것은 컴파일 시 오류가 발생합니다. 이 규칙의 목적을 위해 클래스는 (있는 경우) 직접 기본 클래스에 의존하며, (있는 경우) 중첩된 가장 가까운 바깥쪽 클래스에도 직접 의존합니다. 이 정의에 따르면 클래스가 의존하는 클래스의 전체 집합은 직접 의존 관계의 전이적 폐쇄입니다.

: 예제

class A : A {}

클래스가 스스로에 의존하기 때문에 잘못된 것입니다. 마찬가지로 그 예제도 동일합니다.

class A : B {}
class B : C {}
class C : A {}

클래스가 순환적으로 자체에 의존하기 때문에 오류가 발생합니다. 마지막으로, 예제

class A : B.C {}
class B : A
{
    public class C {}
}

A는 B.C에 의존하고, B.CA에 의존하며, A은 에 순환적으로 의존하기 때문에 컴파일 시간 오류가 발생합니다.

끝 예제

클래스는 클래스 내에 중첩된 클래스에 의존하지 않습니다.

예제: 다음 코드에서

class A
{
    class B : A {}
}

BA가 직접 기본 클래스이자 바로 바깥쪽 클래스이기 때문에 A에 의존하지만, ABB의 기본 클래스도 아니고 바깥쪽 클래스도 아니기 때문에 A에 의존하지 않습니다. 따라서 이 예제는 유효합니다.

끝 예제

봉인된 클래스에서 파생할 수 없습니다.

예제: 다음 코드에서

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

클래스는 봉인된 클래스 BA에서 파생하려고 시도하므로 오류가 발생합니다.

끝 예제

15.2.4.3 인터페이스 구현

class_base 사양에는 인터페이스 형식 목록이 포함될 수 있으며, 이 경우 클래스는 지정된 인터페이스 형식을 구현했다고 합니다. 제네릭 형식 선언(§15.3.9.7) 내에 선언된 중첩 형식을 포함하여 생성된 클래스 형식의 경우 지정된 인터페이스의 각 type_parameter 대해 생성된 형식의 해당 type_argument 대체하여 구현된 각 인터페이스 형식을 가져옵니다.

여러 부분으로 선언된 형식의 인터페이스 집합(§15.2.7)은 각 파트에 지정된 인터페이스의 합합입니다. 특정 인터페이스는 각 파트에서 한 번만 이름을 지정할 수 있지만 여러 파트는 동일한 기본 인터페이스의 이름을 지정할 수 있습니다. 지정된 인터페이스의 각 멤버에 대해 하나의 구현만 있어야 합니다.

: 다음에서:

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

클래스 C 에 대한 기본 인터페이스 집합은 IA, IBIC.

끝 예제

일반적으로 각 파트는 해당 부분에 선언된 인터페이스의 구현을 제공합니다. 그러나 이는 요구 사항이 아닙니다. 파트는 다른 부분에 선언된 인터페이스에 대한 구현을 제공할 수 있습니다.

예제:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

끝 예제

클래스 선언에 지정된 기본 인터페이스는 인터페이스 형식(§8.4, §18.2)을 생성할 수 있습니다. 기본 인터페이스는 범위에 있는 형식 매개 변수를 포함할 수 있지만 자체적으로 형식 매개 변수가 될 수 없습니다.

예제: 다음 코드는 클래스가 생성된 형식을 구현하고 확장하는 방법을 보여 줍니다.

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

끝 예제

인터페이스 구현은 §18.6에서 자세히 설명합니다.

15.2.5 형식 매개 변수 제약 조건

제네릭 형식 및 메서드 선언은 필요에 따라 type_parameter_constraints_clause포함하여 형식 매개 변수 제약 조건을 지정할 수 있습니다.

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

type_parameter_constraints_clause 토큰 where과 형식 매개 변수의 이름, 콜론 및 해당 형식 매개 변수에 대한 제약 조건 목록으로 구성됩니다. 각 형식 매개 변수에 대해 최대 하나의 where 절이 있을 수 있으며 where 절은 순서에 따라 나열될 수 있습니다. getset 속성 접근자의 토큰과 마찬가지로, where 토큰 또한 키워드가 아닙니다.

절에 where 지정된 제약 조건 목록에는 단일 기본 제약 조건, 하나 이상의 보조 제약 조건 및 생성자 제약 new()조건과 같은 구성 요소가 순서대로 포함될 수 있습니다.

기본 제약 조건은 클래스 형식, 참조 형식 제약 조건, 값 형식 제약 조건, null 불가 제약 조건 또는 비관리형 형식 제약 조건일 수 있습니다. 클래스 형식 및 참조 형식 제약 조건에는 nullable_type_annotation을 포함할 수 있습니다.

보조 제약 조건은 interface_type 또는 type_parameter 일 수 있으며, nullable_type_annotation이 뒤따를 수 있습니다. nullable_type_annotation이 있으면, 이 형식 인수가 제약 조건을 충족시키는 non-nullable 참조 형식에 상응하는 nullable 참조 형식임을 나타냅니다.

참조 형식 제약 조건은 형식 매개 변수에 사용되는 형식 인수가 참조 형식이 되도록 지정합니다. 모든 클래스 형식, 인터페이스 형식, 대리자 형식, 배열 형식 및 참조 형식으로 알려진 형식 매개 변수(아래에 정의된 대로)는 이 제약 조건을 충족합니다.

클래스 형식, 참조 형식 제약 조건 및 보조 제약 조건에는 nullable 형식 주석이 포함될 수 있습니다. 형식 매개 변수에 이 주석이 없거나 없는 경우 형식 인수에 대한 Null 허용 여부 기대치를 나타냅니다.

  • 제약 조건에 nullable 형식 주석이 포함되지 않은 경우, 형식 인수는 null이 될 수 없는 참조 형식이어야 합니다. 형식 인수가 nullable 참조 형식인 경우 컴파일러에서 경고를 표시할 수 있습니다.
  • 제약 조건에 nullable 형식 주석이 포함된 경우 nullable이 아닌 참조 형식과 nullable 참조 형식 모두에 의해 제약 조건이 충족됩니다.

형식 인수의 null 허용 여부가 형식 매개 변수의 null 허용 여부와 일치하지 않아도 합니다. 형식 매개 변수의 null 허용 여부가 형식 인수의 null 허용 여부와 일치하지 않는 경우 컴파일러에서 경고를 발생시킬 수 있습니다.

참고: 형식 인수가 nullable 참조 형식임을 지정하려면 nullable 형식 주석을 제약 조건(사용 T : class 또는 T : BaseClass)으로 추가하지 말고 제네릭 선언 전체에서 형식 인수에 해당하는 nullable 참조 형식을 나타내는 데 사용합니다 T? . 끝 메모

nullable 형식 주석 ?은 제약이 없는 형식 인수에서 사용할 수 없습니다.

형식 인수가 nullable 참조 형식인 경우에 형식 매개 변수 TC?의 인스턴스를 T?으로 해석하며, 이는 C?로 해석하지 않습니다.

예제: 다음 예제에서는 형식 인수의 null 허용 여부가 형식 매개 변수 선언의 null 허용 가능성에 미치는 영향을 보여 줍니다.

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

형식 인수가 nullable이 아닌 형식 ? 인 경우 형식 주석은 매개 변수가 해당 nullable 형식임을 나타냅니다. 형식 인수가 이미 null 가능 참조 형식인 경우, 매개 변수는 동일한 null 가능 형식입니다.

끝 예제

null이 아닌 제약 조건은 형식 매개 변수에 사용되는 형식 인수가 null을 허용하지 않는 값 형식 또는 nullable이 아닌 참조 형식이어야 임을 지정합니다. 널이 될 수 없는 값 형식이나 널이 될 수 없는 참조 형식이 아닌 형식 인수도 사용할 수 있지만, 컴파일러가 진단 경고를 발생시킬 수 있습니다.

notnull 키워드가 아니므로 primary_constraint의 not null 제약 조건은 항상 class_type와 구문적으로 모호합니다. 호환성을 위해 이름의 이름 조회(§12.8.4)가 성공하면 해당 이름을 notnull 으로 class_type처리해야 합니다. 그렇지 않으면 NULL이 아닌 제약 조건으로 처리되어야 합니다.

: 다음 클래스는 컴파일러에서 발생할 수 있는 경고를 나타내는 다양한 제약 조건에 대해 다양한 형식 인수를 사용하는 방법을 보여 줍니다.

#nullable enable
public class C { }
public class A<T> where T : notnull { }
public class B1<T> where T : C { }
public class B2<T> where T : C? { }
class Test
{
    static void M()
    {
        // nonnull constraint allows nonnullable struct type argument
        A<int> x1;
        // possible warning: nonnull constraint prohibits nullable struct type argument
        A<int?> x2;
        // nonnull constraint allows nonnullable class type argument
        A<C> x3;
        // possible warning: nonnull constraint prohibits nullable class type argument
        A<C?> x4;
        // nonnullable base class requirement allows nonnullable class type argument
        B1<C> x5;
        // possible warning: nonnullable base class requirement prohibits nullable class type argument
        B1<C?> x6;
        // nullable base class requirement allows nonnullable class type argument
        B2<C> x7;
        // nullable base class requirement allows nullable class type argument
        B2<C?> x8;
    }
}

값 형식 제약 조건은 형식 매개 변수에 사용되는 형식 인수가 null을 허용하지 않는 값 형식이 되도록 지정합니다. 값 형식 제약 조건이 있는 nullable이 아닌 모든 구조체 형식, 열거형 형식 및 형식 매개 변수는 이 제약 조건을 충족합니다. 값 형식으로 분류되지만 nullable 값 형식(§8.3.12)은 값 형식 제약 조건을 충족하지 않습니다. 값 형식 제약 조건을 가진 형식 매개 변수는 constructor_constraint를 가질 수 없지만, constructor_constraint를 가진 다른 형식 매개 변수에 대한 형식 인수로 사용될 수 있습니다.

참고: System.Nullable<T> 형식은 T에 대한 널이 아닌 값 형식 제약 조건을 지정합니다. 형식을 T??Nullable<Nullable<T>> 형태로 재귀적으로 구성하는 것은 금지됩니다. 끝 메모

관리되지 않는 형식 제약 조건은 형식 매개 변수에 사용되는 형식 인수가 nullable이 아닌 관리되지 않는 형식(§8.8)으로 지정합니다.

키워드가 아니기 때문에 unmanagedprimary_constraint에서 관리되지 않는 제약 조건은 항상 class_type과 구문적으로 모호합니다. 호환성을 위해 이름 조회(§12.8.4)가 성공하면 이름이 unmanagedclass_type 처리됩니다. 그렇지 않으면 관리되지 않는 제약 조건으로 처리됩니다.

포인터 형식은 형식 인수가 될 수 없으며 관리되지 않는 형식임에도 불구하고 관리되지 않는 형식 제약 조건을 충족하지 않습니다.

제약 조건이 클래스 형식, 인터페이스 형식 또는 형식 매개 변수인 경우 해당 형식은 해당 형식 매개 변수에 사용되는 모든 형식 인수가 지원하는 최소 "기본 형식"을 지정합니다. 생성된 형식 또는 제네릭 메서드를 사용할 때마다 형식 인수는 컴파일 타임에 형식 매개 변수의 제약 조건에 대해 검사됩니다. 제공된 형식 인수는 §8.4.5설명된 조건을 충족해야 합니다.

class_type 제약 조건은 다음 규칙을 충족해야 합니다.

  • 형식은 클래스 형식이어야 합니다.
  • 형식은 sealed 이면 안 됩니다.
  • 형식은 다음 형식 System.Array 중 하나가 아니어야 합니다. 또는 System.ValueType.
  • 형식은 object 이면 안 됩니다.
  • 지정된 형식 매개 변수에 대한 최대 하나의 제약 조건은 클래스 형식일 수 있습니다.

interface_type 제약 조건으로 지정된 형식은 다음 규칙을 충족해야 합니다.

  • 형식은 인터페이스 형식이어야 합니다.
  • 지정된 where 절에서 형식을 두 번 이상 지정해서는 안 됩니다.

두 경우 모두 제약 조건에는 생성된 형식의 일부로 연결된 형식 또는 메서드 선언의 형식 매개 변수가 포함될 수 있으며 선언되는 형식이 포함될 수 있습니다.

형식 매개 변수 제약 조건으로 지정된 모든 클래스 또는 인터페이스 형식은 선언되는 제네릭 형식 또는 메서드만큼 액세스 가능(§7.5.5)이어야 합니다.

type_parameter 제약 조건으로 지정된 형식은 다음 규칙을 충족해야 합니다.

  • 형식은 형식 매개 변수여야 합니다.
  • 지정된 where 절에서 형식을 두 번 이상 지정해서는 안 됩니다.

또한 형식 매개 변수의 종속성 그래프 주기가 없어야 합니다. 여기서 종속성은 다음에 정의된 전이적 관계입니다.

  • 형식 매개 변수가 형식 매개 변수 T 에 대한SS 제약 조건으로 사용되는 경우 다음에 따라 달라집니다.T
  • 형식 매개 변수 S가 형식 매개 변수 T에 따라 달라지고, T가 형식 매개 변수 U에 따라 달라지는 경우, S의존합니다U.

이 관계를 고려할 때 형식 매개 변수가 직접 또는 간접적으로 자체적으로 의존하는 것은 컴파일 시간 오류입니다.

모든 제약 조건은 종속 형식 매개 변수 간에 일치해야 합니다. 형식 매개 변수가 형식 매개 변수 ST 에 따라 달라지면 다음을 수행합니다.

  • T 에는 값 형식 제약 조건이 없습니다. 그렇지 않으면 T가 사실상 봉인되어 ST와 동일한 유형으로 강제되어 두 유형 매개 변수가 필요하지 않게 됩니다.
  • 값 형식 제약이 있는 경우 S, Tclass_type 제약 조건이 없어야 합니다.
  • Sclass_type 제약 조건이 있고 AT 제약 조건이 있는 경우, B에서 A로의 ID 변환 또는 암시적 참조 변환이나 B에서 B로의 암시적 참조 변환이 있어야 합니다.
  • S 또한 형식 매개 변수 U에 의존하고, Uclass_type 제약 조건이 있으며, AT 제약 조건이 있는 경우, 에서 B로의 ID 변환 또는 암시적 참조 변환이거나, A에서 B로의 암시적 참조 변환이 있어야 합니다.

S에 값 형식 제약 조건이 있고 T에 참조 형식 제약 조건이 있는 것은 유효합니다. 실제로 이 제한은 T 형식 System.Object, System.ValueTypeSystem.Enum모든 인터페이스 형식으로 제한됩니다.

형식 매개 변수의 where 절에 생성자 제약 조건(형식 new()포함)이 포함된 경우 연산자를 사용하여 new 형식의 인스턴스를 만들 수 있습니다(§12.8.17.2). 생성자 제약 조건이 있는 형식 매개 변수에 사용되는 모든 형식 인수는 값 형식, public 매개 변수가 없는 생성자가 있는 비 추상 클래스 또는 값 형식 제약 조건 또는 생성자 제약 조건이 있는 형식 매개 변수여야 합니다.

type_parameter_constraintsprimary_constraintstruct 또는 unmanaged이(가) 있는 경우, constructor_constraint를 함께 갖는 것은 컴파일 시간 오류입니다.

: 다음은 제약 조건의 예입니다.

interface IPrintable
{
    void Print();
}

interface IComparable<T>
{
    int CompareTo(T value);
}

interface IKeyProvider<T>
{
    T GetKey();
}

class Printer<T> where T : IPrintable {...}
class SortedList<T> where T : IComparable<T> {...}

class Dictionary<K,V>
    where K : IComparable<K>
    where V : IPrintable, IKeyProvider<K>, new()
{
    ...
}

다음 예제에서는 형식 매개 변수의 종속성 그래프 순환이 발생하므로 오류가 발생합니다.

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

다음 예제에서는 잘못된 추가 상황을 보여 줍니다.

class Sealed<S,T>
    where S : T
    where T : struct // Error, `T` is sealed
{
    ...
}

class A {...}
class B {...}

class Incompat<S,T>
    where S : A, T
    where T : B // Error, incompatible class-type constraints
{
    ...
}

class StructWithClass<S,T,U>
    where S : struct, T
    where T : U
    where U : A // Error, A incompatible with struct
{
    ...
}

끝 예제

타입의 동적 삭제C 타입으로 다음과 같이 생성됩니다: Cₓ

  • 중첩 형식 COuter.Inner인 경우 Cₓ도 중첩 형식 Outerₓ.Innerₓ입니다.
  • 형식 인수 C가 있는 경우 Cₓ이(가) 생성된 형식 G<A¹, ..., Aⁿ>A¹, ..., Aⁿ이고, Cₓ는 생성된 형식 G<A¹ₓ, ..., Aⁿₓ>입니다.
  • C가 배열 형식E[]이면, Cₓ가 배열 형식Eₓ[]입니다.
  • C가 동적이면 Cₓobject입니다.
  • 그렇지 않으면 CₓC입니다.

유효 기본 클래스는 형식 매개변수 T의 경우 다음과 같이 정의됩니다.

다음과 같은 형식 집합을 살펴보겠습니다 R .

  • 형식 매개 변수 TR 각 제약 조건에는 유효 기본 클래스가 포함됩니다.
  • 구조체 형식인 T의 각 제약 조건에는 RSystem.ValueType가 포함됩니다.
  • 열거형 형식인 T 각 제약 조건이 R를 포함합니다 System.Enum.
  • 각 제약 조건 중 대리자 형식인 T에 대해, R는 해당하는 동적 지우기를 포함합니다.
  • 배열 형식인 T 각 제약 조건에는 RSystem.Array를 포함합니다.
  • 클래스 형식인 T의 각 제약 조건에 대해, R는 해당 동적 지우기를 포함합니다.

그러면

  • 값 형식 제약 조건이 있는 경우 T 유효 기본 클래스는 .입니다 System.ValueType.
  • 그렇지 않으면 R가 비어 있으면 유효한 기본 클래스는 object입니다.
  • 그렇지 않으면 집합T의 유효 기본 클래스는 가장 포괄적인 형식(§10.5.3)입니다. 집합에 포괄 형식이 없으면 유효 기본 클래스 T 는 .입니다 object. 일관성 규칙은 가장 포괄적인 형식이 있는지 확인합니다.

형식 매개 변수가 기본 메서드에서 제약 조건이 상속되는 메서드 형식 매개 변수인 경우 유효 기본 클래스는 형식 대체 후에 계산됩니다.

이러한 규칙은 유효 기본 클래스가 항상 class_type 보장합니다.

타입 매개변수의 효과적인 인터페이스 세트는 다음과 같이 정의됩니다.

  • Tsecondary_constraints가 없으면, 유효 인터페이스 집합은 비어 있습니다.
  • Tinterface_type에 제약 조건이 있지만 type_parameter에 제약 조건이 없는 경우, 해당 유효한 인터페이스 집합은 그 interface_type 제약 조건의 동적 소거의 집합입니다.
  • 에 interface_type 제약 조건이 없고 type_parameter 제약 조건이 있는 경우, 해당 유효 인터페이스 집합은 type_parameter 제약 조건의 유효 인터페이스 집합들의 결합입니다.
  • Tinterface_type 제약 조건과 type_parameter 제약 조건을 모두 가지고 있다면, 그것의 유효 인터페이스 집합은 그 interface_type 제약 조건의 동적 지우기 집합과 해당 type_parameter 제약 조건의 유효 인터페이스 집합의 합집합입니다.

형식 매개 변수는 참조 형식 제약 조건이 있거나 유효한 기본 클래스가 아닌 objectSystem.ValueType경우 참조 형식으로 알려져 있습니다. 형식 매개 변수는 참조 형식으로 확인되고 널이 될 수 없는 참조 형식 제약 조건이 있는 경우, 널이 될 수 없는 참조 형식으로 간주됩니다.

제약 조건이 내포된 인스턴스 멤버에 액세스하는 데 제한된 형식 매개 변수 형식의 값을 사용할 수 있습니다.

: 다음에서:

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

IPrintable의 메서드는 x에서 직접 호출할 수 있습니다. 왜냐하면 T가 항상 IPrintable을 구현하도록 제한되기 때문입니다.

끝 예제

부분 제네릭 형식 선언에 제약 조건이 포함된 경우 제약 조건은 제약 조건을 포함하는 다른 모든 부분에 동의해야 합니다. 특히 제약 조건을 포함하는 각 부분에는 동일한 형식 매개 변수 집합에 대한 제약 조건이 있어야 하며 각 형식 매개 변수에 대해 기본, 보조 및 생성자 제약 조건 집합은 동일해야 합니다. 두 제약 조건 집합은 동일한 멤버를 포함하는 경우 동일합니다. 부분 제네릭 형식의 일부가 형식 매개 변수 제약 조건을 지정하지 않는 경우 형식 매개 변수는 제약이 없는 것으로 간주됩니다.

예제:

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

는 제약 조건(처음 두 부분)을 포함하는 파트가 동일한 형식 매개 변수 집합에 대해 동일한 기본, 보조 및 생성자 제약 조건 집합을 효과적으로 지정하기 때문에 정확합니다.

끝 예제

15.2.6 클래스 본문

클래스의 class_body 해당 클래스의 멤버를 정의합니다.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 부분 형식 선언

한정자는 partial 여러 부분에서 클래스, 구조체 또는 인터페이스 형식을 정의할 때 사용됩니다. partial 한정자는 상황별 키워드(§6.4.4)이며 키워드 class, structinterface바로 앞에 특별한 의미가 있습니다. 부분 형식은 부분 메서드 선언(§15.6.9)을 포함할 수 있습니다.

부분 형식 선언의 각 부분에는 partial 수정자가 포함되어야 하며, 다른 부분들과 같은 네임스페이스 또는 포함 형식 내에 선언되어야 합니다. partial 한정자는 형식 선언의 추가 부분이 다른 곳에 있을 수 있지만 이러한 추가 파트의 존재는 요구 사항이 아니라 한정자를 포함하는 partial 형식의 유일한 선언에 유효합니다. 부분 형식의 선언 하나만 기본 클래스 또는 구현된 인터페이스를 포함할 수 있습니다. 그러나 기본 클래스 또는 구현된 인터페이스의 모든 선언은 지정된 형식 인수의 null 허용 여부를 포함하여 일치해야 합니다.

부분 형식의 모든 부분은 컴파일 시간에 병합될 수 있도록 함께 컴파일되어야 합니다. 부분 형식은 특히 이미 컴파일된 형식을 확장할 수 없습니다.

중첩 형식은 partial 한정자를 사용하여 여러 부분으로 선언할 수 있습니다. 일반적으로 포함 형식도 사용하여 partial 선언되며 중첩된 형식의 각 부분은 포함하는 형식의 다른 부분에서 선언됩니다.

: 다음 partial 클래스는 서로 다른 컴파일 단위에 있는 두 부분으로 구현됩니다. 첫 번째 부분은 데이터베이스 매핑 도구에서 생성된 컴퓨터이고 두 번째 부분은 수동으로 작성됩니다.

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

위의 두 부분이 함께 컴파일되면 결과 코드는 다음과 같이 클래스가 단일 단위로 작성된 것처럼 동작합니다.

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

끝 예제

부분 형식 선언의 다른 부분의 형식 또는 형식 매개 변수에 지정된 특성의 처리는 §22.3설명합니다.

15.3 클래스 멤버

15.3.1 일반

클래스의 멤버는 class_member_declaration으로 도입된 멤버와 직접 기본 클래스에서 상속된 멤버로 구성됩니다.

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

클래스의 멤버는 다음 범주로 나뉩니다.

  • 클래스와 연결된 상수 값을 나타내는 상수(§15.4)입니다.
  • 클래스의 변수인 필드(§15.5)입니다.
  • 클래스(§15.6)에서 수행할 수 있는 계산 및 작업을 구현하는 메서드입니다.
  • 명명된 특성 및 해당 특성을 읽고 쓰는 것과 관련된 작업을 정의하는 속성(§15.7).
  • 클래스(§15.8)에서 생성할 수 있는 알림을 정의하는 이벤트입니다.
  • 인덱서- 클래스의 인스턴스를 배열과 동일한 방식으로(구문적으로) 인덱싱할 수 있도록 허용합니다(§15.9).
  • 클래스의 인스턴스에 적용할 수 있는 식 연산자를 정의하는 연산자입니다(§15.10).
  • 클래스의 인스턴스를 초기화하는 데 필요한 작업을 구현하는 인스턴스 생성자(§15.11)
  • 클래스 인스턴스가 영구적으로 삭제되기 전에 수행할 작업을 구현하는 종료자(§15.13).
  • 정적 생성자- 클래스 자체를 초기화하는 데 필요한 작업을 구현합니다(§15.12).
  • 클래스(§14.7)에 로컬인 형식을 나타내는 형식입니다.

class_declaration은 새 선언 공간(§7.3)을 만들며, 이때 class_declaration에 직접 포함된 type_parameterclass_member_declaration들이 이 선언 공간에 새로운 멤버로 추가됩니다. 다음 규칙은 class_member_declaration적용됩니다.

  • 인스턴스 생성자, 종료자 및 정적 생성자는 바로 상위 클래스와 동일한 이름을 가져야 합니다. 다른 모든 구성원은 직접 포함하는 클래스의 이름과 다른 이름을 가져야 합니다.

  • 클래스 선언의 type_parameter_list 형식 매개 변수의 이름은 동일한 type_parameter_list 다른 모든 형식 매개 변수의 이름과 달라야 하며 클래스의 이름 및 클래스의 모든 멤버 이름과 다릅니다.

  • 형식의 이름은 동일한 클래스에 선언된 형식이 아닌 모든 멤버의 이름과 다릅니다. 두 개 이상의 형식 선언이 동일한 정규화된 이름을 공유하는 경우 선언에는 partial 한정자(§15.2.7)가 있어야 하며 이러한 선언이 결합되어 단일 형식을 정의합니다.

참고: 형식 선언의 정규화된 이름은 형식 매개 변수 수를 인코딩하므로 형식 매개 변수 수가 다르면 두 개의 고유 형식이 동일한 이름을 공유할 수 있습니다. 끝 메모

  • 상수, 필드, 속성 또는 이벤트의 이름은 동일한 클래스에 선언된 다른 모든 멤버의 이름과 다릅니다.

  • 메서드의 이름은 동일한 클래스에 선언된 다른 모든 비 메서드의 이름과 다릅니다. 또한 메서드의 서명(§7.6)은 동일한 클래스에 선언된 다른 모든 메서드의 서명과 달라야 하며, 동일한 클래스에 선언된 두 메서드의 서명은 in, out, ref에 의해서만 달라져서는 안 됩니다.

  • 인스턴스 생성자의 서명은 동일한 클래스에 선언된 다른 모든 인스턴스 생성자의 서명과 달라야 하며, 동일한 클래스에 선언된 두 개의 생성자는 서명이 오직 refout으로만 차이가 나서는 안 됩니다.

  • 인덱서의 서명은 동일한 클래스에 선언된 다른 모든 인덱서의 서명과 다릅니다.

  • 연산자의 서명은 동일한 클래스에 선언된 다른 모든 연산자의 서명과 다릅니다.

클래스의 상속된 멤버(§15.3.4)는 클래스의 선언 공간에 속하지 않습니다.

참고: 따라서 파생 클래스는 상속된 멤버와 동일한 이름 또는 시그니처를 가진 멤버를 선언할 수 있습니다(상속된 멤버는 사실상 숨겨짐). 끝 메모

여러 부분으로 선언된 형식의 멤버 집합(§15.2.7)은 각 부분에서 선언된 멤버의 합합입니다. 형식 선언의 모든 부분의 본문은 동일한 선언 공간(§7.3)을 공유하고 각 멤버의 범위(§7.7)는 모든 부분의 본문으로 확장됩니다. 어떤 멤버의 접근성 도메인에는 항상 둘러싸는 타입의 모든 부분이 포함됩니다. 하나의 부분에 선언된 프라이빗 멤버는 다른 부분에서 자유롭게 접근할 수 있습니다. 해당 멤버에 partial 한정자가 없는 한 형식의 둘 이상의 부분에서 동일한 멤버를 선언하는 것은 컴파일 시간 오류입니다.

예제:

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M();        // Ok, defining partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M() { }     // Ok, implementing partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int z;
    }
}

끝 예제

필드 초기화 순서는 C# 코드 내에서 중요할 수 있으며 § 15.5.6.1정의된 대로 일부 보장이 제공됩니다. 그렇지 않으면 형식 내 멤버의 순서는 거의 중요하지 않지만 다른 언어 및 환경과 상호 작용할 때 중요할 수 있습니다. 이러한 경우 여러 부분으로 선언된 형식 내의 멤버 순서는 정의되지 않습니다.

15.3.2 인스턴스 유형

각 클래스 선언에는 연결된 인스턴스 형식이 있습니다. 제네릭 클래스 선언의 경우 형식 선언에서 생성된 형식(§8.4)을 만들어 인스턴스 형식을 구성하고 제공된 각 형식 인수는 해당 형식 매개 변수입니다. 인스턴스 형식은 형식 매개 변수를 사용하므로 형식 매개 변수가 범위에 있는 경우에만 사용할 수 있습니다. 즉, 클래스 선언 내에 있습니다. 인스턴스 형식은 클래스 선언 내에 작성된 코드의 this 형식입니다. 제네릭이 아닌 클래스의 경우 인스턴스 형식은 단순히 선언된 클래스입니다.

예제: 다음은 인스턴스 형식과 함께 여러 클래스 선언을 보여 줍니다.

class A<T>             // instance type: A<T>
{
    class B {}         // instance type: A<T>.B
    class C<U> {}      // instance type: A<T>.C<U>
}
class D {}             // instance type: D

끝 예제

15.3.3 생성된 형식의 멤버

생성된 형식의 상속되지 않은 멤버는 멤버 선언의 각 type_parameter 대해 생성된 형식의 해당 type_argument 대체하여 가져옵니다. 대체 프로세스는 형식 선언의 의미 체계적 의미를 기반으로 하며 단순히 텍스트 대체가 아닙니다.

: 제네릭 클래스 선언이 지정된 경우

class Gen<T,U>
{
    public T[,] a;
    public void G(int i, T t, Gen<U,T> gt) {...}
    public U Prop { get {...} set {...} }
    public int H(double d) {...}
}

생성된 형식 Gen<int[],IComparable<string>> 에는 다음 멤버가 있습니다.

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

제네릭 클래스 선언 a 에 있는 멤버 Gen 의 형식은 "2차원 배열T"이므로 위의 생성된 형식에 있는 멤버 a 의 형식은 "1차원 배열의 int2차원 배열 또는 "int[,][]입니다.

끝 예제

인스턴스 함수 멤버 내에서 형식은 포함하는 선언의 this 인스턴스 형식(§15.3.2)입니다.

제네릭 클래스의 모든 멤버는 직접 또는 생성된 형식의 일부로 모든 묶은 클래스의 형식 매개 변수를 사용할 수 있습니다. 런타임에 닫힌 특정 생성 형식(§8.4.3)을 사용하면 형식 매개 변수의 각 사용이 생성된 형식에 제공된 형식 인수로 바뀝니다.

예제:

class C<V>
{
    public V f1;
    public C<V> f2;

    public C(V x)
    {
        this.f1 = x;
        this.f2 = this;
    }
}

class Application
{
    static void Main()
    {
        C<int> x1 = new C<int>(1);
        Console.WriteLine(x1.f1);              // Prints 1

        C<double> x2 = new C<double>(3.1415);
        Console.WriteLine(x2.f1);              // Prints 3.1415
    }
}

끝 예제

15.3.4 상속

클래스 는 직접 기본 클래스의 멤버를 상속합니다 . 상속은 클래스가 인스턴스 생성자, 종료자, 및 정적 생성자를 제외한, 직접적인 기본 클래스의 모든 멤버를 암시적으로 포함한다는 것을 의미합니다. 상속의 몇 가지 중요한 측면은 다음과 같습니다.

  • 상속은 전이적입니다. CB에서 파생되고, BA에서 파생된 경우, CB에 선언된 멤버뿐만 아니라 A에 선언된 멤버도 상속합니다.

  • 파생 클래스 는 직접 기본 클래스를 확장합니다 . 파생된 클래스를 상속하는 대상에 새 멤버를 추가할 수 있지만 상속된 멤버의 정의를 제거할 수 없습니다.

  • 인스턴스 생성자, 종료자 및 정적 생성자는 상속되지 않지만 다른 모든 멤버는 선언된 접근성(§7.5)에 관계없이 상속됩니다. 그러나 선언된 접근성에 따라 상속된 멤버는 파생 클래스에서 액세스할 수 없을 수 있습니다.

  • 파생 클래스는 이름이나 서명이 같은 새 멤버를 선언하여 상속된 멤버를 숨길있습니다(§7.7.2.3). 그러나 상속된 멤버를 숨기면 해당 멤버가 제거되지 않으며 파생 클래스를 통해 해당 멤버에 직접 액세스할 수 없게 됩니다.

  • 클래스의 인스턴스에는 클래스 및 해당 기본 클래스에 선언된 모든 인스턴스 필드 집합이 포함되며, 파생 클래스 형식에서 기본 클래스 형식으로의 암시적 변환(§10.2.8)이 존재합니다. 따라서 일부 파생 클래스의 인스턴스에 대한 참조는 해당 기본 클래스의 인스턴스에 대한 참조로 처리될 수 있습니다.

  • 클래스는 가상 메서드, 속성, 인덱서 및 이벤트를 선언할 수 있으며 파생 클래스는 이러한 함수 멤버의 구현을 재정의할 수 있습니다. 이를 통해 클래스는 함수 멤버 호출에 의해 수행되는 작업이 해당 함수 멤버가 호출되는 인스턴스의 런타임 형식에 따라 달라지는 다형적 동작을 나타낼 수 있습니다.

생성된 클래스 형식의 상속된 멤버는 즉시 기본 클래스 형식(§15.2.4.2)의 멤버이며, 이 멤버는 base_class_specification 해당 형식 매개 변수가 발생할 때마다 생성된 형식의 형식 인수를 대체하여 찾습니다. 이러한 멤버는 멤버 선언의 각 type_parameter에 대해 base_class_specification의 해당 type_argument으로 대체되어 변환됩니다.

예제:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

위의 코드에서 생성된 형식에는 형식 매개 변수D<int>에 대한 형식 int 인수 G(string s) 를 대체하여 가져온 상속되지 않은 멤버 공용 intT 이 있습니다. D<int> 에는 클래스 선언 B에서 상속된 멤버도 있습니다. 이 상속된 멤버는 기본 클래스 사양 B<int[]>D<int>int으로 대체하여 기본 클래스 형식 TB<T[]>을 먼저 결정함으로써 결정됩니다. 그런 다음, 형식 인수 B로, int[]U에서 public U F(long index)를 대체하여 상속 멤버 public int[] F(long index)를 생성합니다.

끝 예제

15.3.5 새 한정자

class_member_declaration 상속된 멤버와 동일한 이름 또는 서명을 가진 멤버를 선언할 수 있습니다. 이 경우 파생 클래스 멤버는 기본 클래스 멤버를 숨깁니다 . 멤버가 상속된 멤버를 숨기는 경우의 정확한 사양은 §7.7.2.3을 참조하세요.

상속된 멤버 M에 액세스할 수 있고 이미 를 숨기는 다른 상속된 액세스 가능한 멤버 N이 없을 경우 M한 것으로 간주됩니다. 상속된 멤버를 암시적으로 숨기는 것은 오류로 간주되지 않지만 파생 클래스 멤버의 선언에 파생 멤버가 기본 멤버를 숨기려는 것임을 명시적으로 나타내는 new 한정자가 포함되지 않는 한 컴파일러는 경고를 발생합니다. 중첩 형식의 부분 선언(§15.2.7)에 하나 이상의 부분이 한정자를 포함하는 new 경우 중첩된 형식이 사용 가능한 상속된 멤버를 숨기는 경우 경고가 발생하지 않습니다.

new 한정자가 사용 가능한 상속된 멤버를 숨기지 않는 선언에 포함된 경우, 그에 따른 경고가 출력됩니다.

15.3.6 액세스 한정자

class_member_declaration는 허용된 선언된 접근성 종류 (§7.5.2) 중 하나를 가질 수 있습니다: public, protected internal, protected, private protected, internal 또는 private. protected internalprivate protected 조합을 제외하고 둘 이상의 액세스 한정자를 지정하는 것은 컴파일 시간 오류입니다. class_member_declaration이(가) 액세스 한정자를 포함하지 않으면 private로 간주됩니다.

15.3.7 구성 요소 형식

멤버 선언에 사용되는 형식을 해당 멤버의 구성 요소 형식이라고 합니다. 가능한 구성 요소 형식은 상수, 필드, 속성, 이벤트 또는 인덱서의 형식, 메서드 또는 연산자의 반환 형식 및 메서드, 인덱서, 연산자 또는 인스턴스 생성자의 매개 변수 형식입니다. 구성원의 구성 유형은 최소한 해당 멤버 자체만큼 액세스할 수 있어야 합니다(§7.5.5).

15.3.8 정적 및 인스턴스 멤버

클래스의 멤버는 정적 멤버 또는 인스턴스 멤버입니다.

참고: 일반적으로 정적 멤버를 클래스에 속하는 것으로 간주하고 인스턴스 멤버를 개체(클래스 인스턴스)에 속하는 것으로 생각하는 것이 유용합니다. 끝 메모

필드, 메서드, 속성, 이벤트, 연산자 또는 생성자 선언에 static 한정자가 포함되어 있으면 정적 멤버를 선언합니다. 또한 상수 또는 형식 선언은 정적 멤버를 암시적으로 선언합니다. 정적 멤버의 특징은 다음과 같습니다.

  • 정적 멤버 M가 형식 에 포함된 member_access (E.M)에서 참조될 때, E는 멤버 M를 포함하는 형식을 나타내야 합니다. E로 인스턴스를 나타내는 것은 컴파일 시간 오류입니다.
  • 제네릭이 아닌 클래스의 정적 필드는 정확히 하나의 스토리지 위치를 식별합니다. 제네릭이 아닌 클래스의 인스턴스가 몇 개나 만들어지더라도 정적 필드의 복사본은 하나뿐입니다. 각 고유의 닫힌 생성 형식(§8.4.3)에는 닫힌 생성된 형식의 인스턴스 수에 관계없이 고유한 정적 필드 집합이 있습니다.
  • 정적 함수 멤버(메서드, 속성, 이벤트, 연산자 또는 생성자)는 특정 인스턴스에서 작동하지 않으며 이러한 함수 멤버에서 이를 참조하는 것은 컴파일 시간 오류입니다.

필드, 메서드, 속성, 이벤트, 인덱서, 생성자 또는 종료자 선언에 정적 한정자가 포함되지 않은 경우 인스턴스 멤버를 선언합니다. 인스턴스 멤버를 비정적 멤버라고도 합니다. 인스턴스 멤버의 특징은 다음과 같습니다.

  • 형식 M (§12.8.7)에서 인스턴스 멤버 가 참조되는 경우, E.M는 멤버 E가 있는 형식의 인스턴스를 나타내야 합니다. E가 형식을 나타내는 것은 바인딩 시간 오류입니다.
  • 클래스의 모든 인스턴스에는 클래스의 모든 인스턴스 필드 집합이 별도로 포함되어 있습니다.
  • 인스턴스 함수 멤버(메서드, 속성, 인덱서, 인스턴스 생성자 또는 종료자)는 주어진 클래스의 인스턴스에 대해 동작하며, 이 인스턴스는 this (§12.8.14)로 액세스할 수 있습니다.

예제: 다음 예제에서는 정적 및 인스턴스 멤버에 액세스하기 위한 규칙을 보여 줍니다.

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

이 메서드는 F 인스턴스 함수 멤버 에서 simple_name (§12.8.4)를 사용하여 인스턴스 멤버와 정적 멤버 모두에 액세스할 수 있음을 보여 줍니다. 이 메서드는 G 정적 함수 멤버에서 simple_name 통해 인스턴스 멤버에 액세스하는 것이 컴파일 시간 오류임을 보여 줍니다. 이 메서드는 Mainmember_access (§12.8.7)에서 인스턴스 멤버는 인스턴스를 통해 접근해야 하고, 정적 멤버는 타입을 통해 접근해야 함을 보여줍니다.

끝 예제

15.3.9 중첩 형식

15.3.9.1 일반

클래스 또는 구조체 내에서 선언된 형식을 중첩된 형식이라고합니다. 컴파일 단위 또는 네임스페이스 내에서 선언된 형식을 중첩되지 않은 형식이라고 합니다.

: 다음 예제에서:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

클래스 B 는 클래스 A내에서 선언되므로 중첩 형식이고 A 클래스는 컴파일 단위 내에서 선언되므로 중첩되지 않은 형식입니다.

끝 예제

15.3.9.2 완전하게 정규화된 이름

중첩 형식 선언의 정규화된 이름(§7.8.3)은 S.N입니다. 여기서 S는 형식 N이 선언된 형식 선언의 정규화된 이름이고, N은 중첩 형식 선언의 정규화되지 않은 이름(§7.8.2)입니다(모든 generic_dimension_specifier 포함(§12.8.18)).

15.3.9.3 접근성 선언됨

중첩되지 않은 형식은 public 또는 internal의 접근성이 선언될 수 있으며 기본적으로 internal의 접근성이 선언됩니다. 중첩 형식에는 이러한 형식의 선언된 접근성과 포함된 형식이 클래스인지 구조체인지에 따라 하나 이상의 추가 형식의 선언된 접근성이 있을 수 있습니다.

  • 클래스에 선언된 중첩 형식은 허용된 종류의 선언된 접근성을 가질 수 있으며, 다른 클래스 멤버와 마찬가지로 기본값 private 은 선언된 접근성입니다.
  • 구조체에 선언된 중첩 형식은 세 가지 형식의 선언된 접근성(publicinternal또는private)을 가질 수 있으며, 다른 구조체 멤버와 마찬가지로 기본값 private 은 선언된 접근성입니다.

: 예제

public class List
{
    // Private data structure
    private class Node
    {
        public object Data;
        public Node? Next;

        public Node(object data, Node? next)
        {
            this.Data = data;
            this.Next = next;
        }
    }

    private Node? first = null;
    private Node? last = null;

    // Public interface
    public void AddToFront(object o) {...}
    public void AddToBack(object o) {...}
    public object RemoveFromFront() {...}
    public object RemoveFromBack() {...}
    public int Count { get {...} }
}

는 프라이빗 중첩 클래스 Node를 선언합니다.

끝 예제

15.3.9.4 숨기기

중첩 형식은 기본 멤버를 숨길 수 있습니다(§7.7.2.2). 숨김 new 을 명시적으로 표현할 수 있도록 중첩된 형식 선언에서 한정자(§15.3.5)가 허용됩니다.

: 예제

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

에 정의된 메서드 M 를 숨기는 중첩 클래스 M 를 보여 줍니다Base.

끝 예제

15.3.9.5 이 액세스

중첩 형식 및 해당 포함 형식은 this_access 관련해서 특별한 관계가 없습니다(§12.8.14). 특히, this 중첩 형식 내에서는 포함하는 타입의 인스턴스 멤버를 참조할 수 없습니다. 중첩된 형식이 포함된 형식의 인스턴스 멤버에 액세스해야 하는 경우 포함된 형식의 인스턴스를 중첩 형식에 대한 생성자 인수로 제공하여 this 액세스를 제공할 수 있습니다.

: 다음 예제

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

이 기술을 보여줍니다. C의 인스턴스는 Nested의 인스턴스를 생성하고, 자신의 this를 Nested의 생성자에 전달하여 C의 인스턴스 멤버에 대한 후속 액세스를 제공합니다.

끝 예제

15.3.9.6 포함 형식의 비공개 및 보호된 멤버 접근

중첩된 형식은 자신의 포함된 형식에 액세스할 수 있는 모든 멤버에 접근할 수 있으며, 여기에는 privateprotected로 선언된 접근성을 가진 포함된 형식의 멤버도 포함됩니다.

: 예제

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

는 중첩된 클래스 C 를 포함하는 클래스 Nested를 보여 줍니다. 내에서 Nested메서드 G 는 정의된 정적 메서드 FC호출하고 F 프라이빗으로 선언된 접근성을 가합니다.

끝 예제

중첩된 형식은 포함하는 형식의 기본 형식에 정의된 보호된 멤버에 액세스할 수도 있습니다.

예제: 다음 코드에서

class Base
{
    protected void F() => Console.WriteLine("Base.F");
}

class Derived: Base
{
    public class Nested
    {
        public void G()
        {
            Derived d = new Derived();
            d.F(); // ok
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.Nested n = new Derived.Nested();
        n.G();
    }
}

중첩 클래스 Derived.NestedF의 인스턴스를 통해 Derived의 기본 클래스 Base에 정의된 보호된 메서드 Derived에 접근합니다.

끝 예제

15.3.9.7 제네릭 클래스의 중첩 형식

제네릭 클래스 선언에는 중첩된 형식 선언이 포함될 수 있습니다. 바깥쪽 클래스의 형식 매개 변수는 중첩된 형식 내에서 사용할 수 있습니다. 중첩된 형식 선언에는 중첩된 형식에만 적용되는 추가 형식 매개 변수가 포함될 수 있습니다.

제네릭 클래스 선언 내에 포함된 모든 형식 선언은 암시적으로 제네릭 형식 선언입니다. 제네릭 형식 내에 중첩된 형식에 대한 참조를 작성할 때 해당 형식 인수를 포함하여 포함된 생성된 형식의 이름을 지정해야 합니다. 그러나 외부 클래스 내에서 중첩된 형식은 한정 없이 사용할 수 있습니다. 외부 클래스의 인스턴스 형식은 중첩된 형식을 생성할 때 암시적으로 사용될 수 있습니다.

예제: 다음은 생성된 Inner생성된 형식을 참조하는 세 가지 올바른 방법을 보여 하며 처음 두 가지는 동일합니다.

class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }

    static void F(T t)
    {
        Outer<T>.Inner<string>.F(t, "abc");    // These two statements have
        Inner<string>.F(t, "abc");             // the same effect
        Outer<int>.Inner<string>.F(3, "abc");  // This type is different
        Outer.Inner<string>.F(t, "abc");       // Error, Outer needs type arg
    }
}

끝 예제

잘못된 프로그래밍 스타일이지만 중첩 형식의 형식 매개 변수는 외부 형식에 선언된 멤버 또는 형식 매개 변수를 숨길 수 있습니다.

예제:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

끝 예제

15.3.10 예약 멤버 이름

15.3.10.1 일반

기본 C# 런타임 구현을 용이하게 하기 위해 속성, 이벤트 또는 인덱서인 각 원본 멤버 선언에 대해 구현은 멤버 선언의 종류, 이름 및 형식(§15.3.10.2, §15.3.10.3, §15.3.10.4)에 따라 두 개의 메서드 서명을 예약해야 합니다. 기본 런타임 구현에서 이러한 예약을 사용하지 않더라도 동일한 범위에서 선언된 멤버가 예약한 서명과 서명이 일치하는 멤버를 선언하는 것은 프로그램의 컴파일 시간 오류입니다.

예약된 이름은 선언을 도입하지 않으므로 멤버 조회에 참여하지 않습니다. 그러나 선언의 관련된 예약 메서드 서명은 상속(§15.3.4)에 참여하며, new 한정자(§15.3.5)를 사용하여 숨길 수 있습니다.

참고: 이러한 이름의 예약은 다음 세 가지 용도로 사용됩니다.

  1. 기본 구현에서 일반 식별자를 C# 언어 기능에 대한 액세스 권한을 얻거나 설정하기 위한 메서드 이름으로 사용할 수 있도록 합니다.
  2. 다른 언어가 C# 언어 기능에 대한 액세스를 얻거나 설정하기 위한 메서드 이름으로 일반 식별자를 사용하여 상호 운용할 수 있도록 합니다.
  3. 모든 C# 구현에서 예약된 멤버 이름의 세부 사항을 일관되게 설정하여 한 컴파일러가 허용하는 원본이 다른 컴파일러에 의해 수락되도록 합니다.

끝 메모

최종자 선언(§15.13)은 서명이 예약되는 원인이 됩니다(§15.3.10.5).

특정 이름은 연산자 메서드 이름(§15.3.10.6)으로 사용하도록 예약되어 있습니다.

15.3.10.2 속성에 예약된 멤버 이름

형식 의 속성 ()의 경우 다음 서명이 예약됩니다.

T get_P();
void set_P(T value);

속성이 읽기 전용이거나 쓰기 전용인 경우에도 두 서명 모두 예약됩니다.

예제: 다음 코드에서

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new void set_P(int value)
    {
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        Console.WriteLine(a.P);
        Console.WriteLine(b.P);
        Console.WriteLine(b.get_P());
    }
}

클래스 A는 읽기 전용 속성 P를 정의하여 get_Pset_P 메서드에 대한 서명을 예약합니다. A 클래스는 B에서 파생되고, AB의 이 예약된 서명을 숨깁니다. 이 예제에서는 출력을 생성합니다.

123
123
456

끝 예제

15.3.10.3 이벤트용으로 예약된 멤버 이름

위임자 형식의 E 이벤트(T)의 경우 다음 서명이 예약됩니다.

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 인덱서용으로 예약된 멤버 이름

매개 변수 목록 을(를) 사용하는 형식 T의 인덱서(L)의 경우, 다음 서명이 예약되어 있습니다.

T get_Item(L);
void set_Item(L, T value);

인덱서가 읽기 전용이거나 쓰기 전용인 경우에도 두 서명이 모두 예약됩니다.

또한 멤버 이름은 Item 예약되어 있습니다.

15.3.10.5 종료자를 위해 예약된 멤버 이름

종료자(§15.13)를 포함하는 클래스의 경우 다음 시그니처가 예약되어 있습니다.

void Finalize();

15.3.10.6 연산자를 위해 예약된 메서드 이름

다음 메서드 이름은 예약되어 있습니다. 많은 연산자가 이 사양에 있지만 일부는 이후 버전에서 사용하도록 예약된 반면 일부는 다른 언어와의 interop용으로 예약되어 있습니다.

메서드 이름 C# 연산자
op_Addition + (바이너리)
op_AdditionAssignment (예약됨)
op_AddressOf (예약됨)
op_Assign (예약됨)
op_BitwiseAnd & (바이너리)
op_BitwiseAndAssignment (예약됨)
op_BitwiseOr \|
op_BitwiseOrAssignment (예약됨)
op_CheckedAddition (나중에 사용하기 위해 예약됨)
op_CheckedDecrement (나중에 사용하기 위해 예약됨)
op_CheckedDivision (나중에 사용하기 위해 예약됨)
op_CheckedExplicit (나중에 사용하기 위해 예약됨)
op_CheckedIncrement (나중에 사용하기 위해 예약됨)
op_CheckedMultiply (나중에 사용하기 위해 예약됨)
op_CheckedSubtraction (나중에 사용하기 위해 예약됨)
op_CheckedUnaryNegation (나중에 사용하기 위해 예약됨)
op_Comma (예약됨)
op_Decrement -- (접두사 및 접미사)
op_Division /
op_DivisionAssignment (예약됨)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (예약됨)
op_Explicit 명시적(축소형) 강제 변환
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit 암시적(확대) 강제 변환
op_Increment ++ (접두사 및 접미사)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (예약됨)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (예약됨)
op_LogicalNot !
op_LogicalOr (예약됨)
op_MemberSelection (예약됨)
op_Modulus %
op_ModulusAssignment (예약됨)
op_MultiplicationAssignment (예약됨)
op_Multiply * (바이너리)
op_OnesComplement ~
op_PointerDereference (예약됨)
op_PointerToMemberSelection (예약됨)
op_RightShift >>
op_RightShiftAssignment (예약됨)
op_SignedRightShift (예약됨)
op_Subtraction - (바이너리)
op_SubtractionAssignment (예약됨)
op_True true
op_UnaryNegation - (단항)
op_UnaryPlus + (단항)
op_UnsignedRightShift (나중에 사용하기 위해 예약됨)
op_UnsignedRightShiftAssignment (예약됨)

15.4 상수

상수는 컴파일 시간에 계산할 수 있는 값인 상수 값을 나타내는 클래스 멤버입니다. constant_declaration은 하나 이상의 지정된 형식의 상수를 도입합니다.

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

constant_declaration 특성 집합(§22), new 한정자(§15.3.5) 및 허용된 종류의 선언된 접근성(§15.3.6) 중 하나를 포함할 수 있습니다. 특성 및 한정자는 constant_declaration 선언된 모든 멤버에 적용됩니다. 상수는 정적 멤버 로 간주되지만 constant_declaration 한정자가 필요하거나 허용되지 static 않습니다. 동일한 한정자가 상수 선언에 여러 번 표시되는 것은 오류입니다.

constant_declaration 형식선언에 의해 도입된 멤버의 형식을 지정합니다. 형식 뒤에는 constant_declarator목록(§13.6.3)이 있으며, 각각 새 멤버가 도입됩니다. constant_declarator는 멤버의 이름을 지정하는 식별자, "=" 토큰, 그리고 멤버의 값을 제공하는 상수 표현식(§12.23)으로 구성됩니다.

상수 선언에 지정된 형식은 , sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum_type 또는 reference_type 중 하나여야 합니다. 각 constant_expression 암시적 변환(§10.2)을 통해 대상 형식 또는 대상 형식으로 변환할 수 있는 형식의 값을 생성해야 합니다.

상수의 형식상수 자체(§7.5.5)만큼 액세스 가능해야 합니다.

상수 값은 simple_name(§12.8.4) 또는 member_access(§12.8.7)를 사용하여 식에서 가져옵니다.

상수는 자체적으로 constant_expression에 참여할 수 있습니다. 따라서 상수는 constant_expression 필요로 하는 모든 구문에 사용될 수 있습니다.

참고: 이러한 구문의 예로는 case 레이블, 문, goto caseenum 멤버 선언, 특성 및 기타 상수 선언이 있습니다. 끝 메모

참고: §12.23에 설명된 대로, constant_expression은 컴파일 타임에 완전히 평가할 수 있는 식입니다. 연산자를 적용하는 것 외에는 string의 null이 아닌 값을 생성할 방법이 없으며, new에서는 new 연산자가 허용되지 않기 때문에, 를 제외한 reference_type의 상수에 가능한 유일한 값은 string입니다. 끝 메모

상수 값의 기호 이름이 필요한 경우, 상수 선언에서 해당 값의 형식이 허용되지 않거나 컴파일 타임 에 constant_expression 값을 계산할 수 없는 경우 읽기 전용 필드(§15.5.3)를 대신 사용할 수 있습니다.

참고: 버전 관리 의미 체계 constreadonly 차이점(§15.5.3.3). 끝 메모

여러 상수 선언은 특성, 한정자 및 형식이 동일한 단일 상수의 여러 선언과 동일합니다.

예제:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

동일하다

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

끝 예제

종속성이 순환 특성이 아닌 한 상수는 동일한 프로그램 내의 다른 상수에 종속될 수 있습니다.

예제: 다음 코드에서

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

컴파일러는 먼저 A.Y평가한 다음, B.Z평가하고, 마지막으로 A.X평가하여 10, 1112값을 생성해야 합니다.

끝 예제

상수 선언은 다른 프로그램의 상수에 따라 달라질 수 있지만 이러한 종속성은 한 방향으로만 가능합니다.

: 위의 예제를 참조하면, 별도의 프로그램에서 선언된 AB이 있을 경우 A.XB.Z에 종속될 수 있지만, B.ZA.Y에 동시에 종속될 수 없습니다. 끝 예제

15.5 필드

15.5.1 일반

필드는 개체 또는 클래스와 연결된 변수를 나타내는 멤버입니다. field_declaration 지정된 형식의 하나 이상의 필드를 소개합니다.

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

field_declaration에는 특성의 집합(§22), new 한정자(§15.3.5), 네 가지 액세스 한정자(§15.3.6)의 유효한 조합, 그리고 static 한정자(§15.5.2)를 포함할 수 있습니다. 또한 field_declarationreadonly 한정자( 단락) 또는 volatile 한정자( 단락)를 포함할 수 있지만 둘 다 포함할 수는 없습니다. 특성 및 한정자는 field_declaration 선언된 모든 멤버에 적용됩니다. 동일한 한정자가 field_declaration 여러 번 표시되는 것은 오류입니다.

field_declaration 형식선언에 의해 도입된 멤버의 형식을 지정합니다. 형식 뒤에는 variable_declarator목록이 있으며 각각 새 멤버가 도입됩니다. variable_declarator 해당 멤버의 이름을 지정하는 식별자, 필요에 따라 "" 토큰 및 =초기 값을 제공하는 variable_initializer(§15.5.6)로 구성됩니다.

필드의 형식적어도 필드 자체(§7.5.5)만큼 액세스할 수 있어야 합니다.

필드 값은 simple_name(§12.8.4), member_access(§12.8.7) 또는 base_access(§12.8.15)를 사용하여 식에서 가져옵니다. 읽기 전용이 아닌 필드의 값은 할당(§12.21)을 사용하여 수정됩니다. 읽기 전용이 아닌 필드의 값은 접두사 증가 및 감소 연산자(§12.8.16) 및 접두사 증가 및 감소 연산자(§12.9.6)를 사용하여 가져오고 수정할 수 있습니다.

여러 필드를 선언하는 필드 선언은 특성, 한정자 및 형식이 동일한 단일 필드의 여러 선언과 동일합니다.

예제:

class A
{
    public static int X = 1, Y, Z = 100;
}

동일하다

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

끝 예제

15.5.2 정적 및 인스턴스 필드

필드 선언에 한 static 정자가 포함된 경우 선언에서 도입된 필드는 정적 필드입니다. 한정자가 없 static 으면 선언에서 도입된 필드는 인스턴스 필드입니다. 정적 필드와 인스턴스 필드는 C#에서 지원하는 여러 종류의 변수(§9)의 두 가지이며, 때때로 정적 변수 인스턴스 변수라고도 합니다.

§15.3.8에서 설명한 대로 클래스의 각 인스턴스는 클래스의 인스턴스 필드의 전체 집합을 포함하지만, 클래스 또는 닫힌 생성 형식의 인스턴스 수에 관계없이 제네릭이 아닌 클래스 또는 닫힌 생성 형식에 대한 정적 필드 집합은 하나뿐입니다.

15.5.3 읽기 전용 필드

15.5.3.1 일반

field_declaration 한정자를 readonly 포함하는 경우 선언에서 도입된 필드는 읽기 전용 필드입니다. 읽기 전용 필드에 대한 직접 할당은 해당 선언의 일부 또는 동일한 클래스의 인스턴스 생성자 또는 정적 생성자에서만 발생할 수 있습니다. (이러한 컨텍스트에서 읽기 전용 필드를 여러 번 할당할 수 있습니다.) 특히 읽기 전용 필드에 대한 직접 할당은 다음 컨텍스트에서만 허용됩니다.

  • 필드를 정의할 때 사용되는 variable_declarator는 선언에 variable_initializer를 포함할 수 있습니다.
  • 인스턴스 필드의 경우, 필드 선언을 포함하는 클래스의 인스턴스 생성자에서; 정적 필드의 경우, 필드 선언을 포함하는 클래스의 정적 생성자에서. 읽기 전용 필드를 출력 또는 참조 매개 변수로 전달하는 것이 유효한 유일한 컨텍스트이기도 합니다.

읽기 전용 필드에 할당하거나 다른 컨텍스트에서 출력 또는 참조 매개 변수로 전달하려고 하면 컴파일 시간 오류가 발생합니다.

15.5.3.2 상수에 정적 읽기 전용 필드 사용

정적 읽기 전용 필드는 상수 값의 기호 이름을 원하는 경우와 const 선언에서 값 형식이 허용되지 않거나 컴파일 타임에 값을 계산할 수 없는 경우에 유용합니다.

예제: 다음 코드에서

public class Color
{
    public static readonly Color Black = new Color(0, 0, 0);
    public static readonly Color White = new Color(255, 255, 255);
    public static readonly Color Red = new Color(255, 0, 0);
    public static readonly Color Green = new Color(0, 255, 0);
    public static readonly Color Blue = new Color(0, 0, 255);

    private byte red, green, blue;

    public Color(byte r, byte g, byte b)
    {
        red = r;
        green = g;
        blue = b;
    }
}

Black컴파일 시간에 값을 계산할 수 없으므로 , White, RedGreenBlue 멤버를 const 멤버로 선언할 수 없습니다. 그러나 대신 선언하는 static readonly 것은 거의 동일한 효과가 있습니다.

끝 예제

15.5.3.3 상수 및 정적 읽기 전용 필드 버전 관리

상수 및 읽기 전용 필드에는 서로 다른 이진 버전 관리 의미 체계가 있습니다. 식에서 상수 값을 참조하는 경우 컴파일 시간에 상수 값을 가져오지만 식이 읽기 전용 필드를 참조하는 경우 런타임까지 필드 값을 가져오지 않습니다.

: 두 개의 개별 프로그램으로 구성된 애플리케이션을 고려합니다.

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Program1Program2 네임스페이스는 별도로 컴파일되는 두 프로그램을 나타냅니다. Program1.Utils.Xstatic readonly 필드로 선언되었기 때문에, Console.WriteLine 명령문이 출력하는 값은 컴파일 시간에 알려지지 않고 런타임에 얻어집니다. 따라서 X의 값이 변경되고 Program1이(가) 다시 컴파일되면, Console.WriteLine이(가) 다시 컴파일되지 않더라도 Program2문은 새 값을 출력합니다. 만약 X가 상수였다면, X가 컴파일될 때 Program2의 값이 얻어졌을 것이고, Program1의 변화에도 Program2가 다시 컴파일되기 전까지는 영향을 받지 않을 것입니다.

끝 예제

15.5.4 휘발성 필드

field_declaration 한정자를 포함하는 volatile 경우 해당 선언에서 도입된 필드는 휘발성 필드입니다. 비휘발성 필드의 경우 명령을 다시 정렬하는 최적화 기법을 사용하면 lock_statement(§13.13)에서 제공하는 것과 같이 동기화 없이 필드에 액세스하는 다중 스레드 프로그램에서 예기치 않고 예측할 수 없는 결과를 초래할 수 있습니다. 이러한 최적화는 컴파일러, 런타임 시스템 또는 하드웨어에서 수행할 수 있습니다. 휘발성 필드의 경우 이러한 다시 정렬 최적화가 제한됩니다.

  • 휘발성 필드의 읽기를 휘발성 읽기라고합니다. 휘발성 읽기는 "획득 의미"를 가지고 있습니다. 즉, 명령 순서에서 이후에 발생하는 메모리 참조보다 먼저 발생할 것이 보장됩니다.
  • 휘발성 필드의 쓰기를 휘발성 쓰기라고합니다. 휘발성 쓰기는 "릴리스 의미"를 갖습니다. 즉, 명령 시퀀스에서 쓰기 명령 이전의 모든 메모리 참조가 완료된 후에 발생하도록 보장됩니다.

제한 사항은 모든 스레드가 다른 스레드에 의해 수행된 휘발성 쓰기를 수행 순서대로 관찰할 수 있도록 합니다. 모든 실행 스레드에서 볼 수 있듯이 휘발성 쓰기의 총 순서를 하나만 제공하는 데는 준수 구현이 필요하지 않습니다. 휘발성 필드의 형식은 다음 중 하나여야 합니다.

  • A 참조_유형.
  • 참조 유형(§15.2.5)으로 알려진 type_parameter.
  • 형식byte, , sbyte, short, ushort, int, uintchar, float, boolSystem.IntPtr또는 System.UIntPtr.
  • 열거형 열거형 기본의 형식을 가지며 byte, sbyte, short, ushort, int, uint 중 하나입니다.

: 예제

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

결과를 생성합니다.

result = 143

이 예제에서 메서드는 메서드 Main 를 실행하는 새 스레드를 Thread2시작합니다. 이 메서드는 값을 비휘발성 필드에 저장result한 다음, 휘발성 필드에 true저장 finished 합니다. 주 스레드는 필드 finished 가 설정 true될 때까지 기다린 다음 필드를 result읽습니다. 선언되었finished으므로 volatile, 주 스레드는 필드 143에서 result 값을 읽어야 합니다. 필드 finishedvolatile 선언되지 않았다면, 저장소 result에 대한 저장소 finished 뒤에 주 스레드에 표시되는 것이 허용되며, 따라서 주 스레드가 필드 result에서 값 0을 읽는 것이 가능해집니다. finishedvolatile 필드로 선언하면 이러한 불일치를 방지할 수 있습니다.

끝 예제

15.5.5 필드 초기화

정적 필드든 인스턴스 필드이든 필드의 초기 값은 필드 형식의 기본값(§9.3)입니다. 이 기본 초기화가 발생하기 전에 필드 값을 확인할 수 없으므로 필드는 항상 초기화된 상태입니다.

: 예제

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

는 출력을 생성합니다.

b = False, i = 0

b 때문에 i 둘 다 자동으로 기본값으로 초기화됩니다.

끝 예제

15.5.6 변수 이니셜라이저

15.5.6.1 일반

필드 선언에는 변수_초기화기가 포함될 수 있습니다. 정적 필드의 경우 변수 이니셜라이저는 클래스 초기화 중에 실행되는 할당 문에 해당합니다. 인스턴스 필드의 경우 변수 이니셜라이저는 클래스의 인스턴스를 만들 때 실행되는 할당 문에 해당합니다.

: 예제

class Test
{
    static double x = Math.Sqrt(2.0);
    int i = 100;
    string s = "Hello";

    static void Main()
    {
        Test a = new Test();
        Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}");
    }
}

는 출력을 생성합니다.

x = 1.4142135623730951, i = 100, s = Hello

정적 필드 이니셜라이저가 실행될 때 x에 할당이 발생하고, 인스턴스 필드 이니셜라이저가 실행될 때 is에 할당이 발생하므로

끝 예제

§15.5.5설명된 기본값 초기화는 변수 이니셜라이저가 있는 필드를 포함하여 모든 필드에 대해 발생합니다. 따라서 클래스가 초기화되면 해당 클래스의 모든 정적 필드가 먼저 기본값으로 초기화된 다음 정적 필드 이니셜라이저가 텍스트 순서로 실행됩니다. 마찬가지로 클래스의 인스턴스가 만들어지면 해당 인스턴스의 모든 인스턴스 필드가 먼저 기본값으로 초기화된 다음 인스턴스 필드 이니셜라이저가 텍스트 순서로 실행됩니다. 동일한 형식에 대한 여러 부분 형식 선언에 필드 선언이 있는 경우 파트의 순서가 지정되지 않습니다. 그러나 각 부분 내에서 필드 이니셜라이저는 순서대로 실행됩니다.

변수 이니셜라이저가 있는 정적 필드가 기본값 상태에서 관찰될 수 있습니다.

: 그러나 스타일 문제로 권장되지는 않습니다. 예제

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

이 동작을 보입니다. 프로그램이 ab의 순환 정의에도 불구하고 유효합니다. 결과는 출력입니다.

a = 1, b = 2

정적 필드 ab는 이니셜라이저가 실행되기 전에 0의 기본값인 int로 초기화되기 때문입니다. a의 초기화가 실행될 때, b의 값은 0이므로 a1으로 초기화됩니다. b의 이니셜라이저가 실행될 때, a의 값은 이미 1이며, 따라서 b2으로 초기화됩니다.

끝 예제

15.5.6.2 정적 필드 초기화

클래스의 정적 필드 변수 이니셜라이저는 클래스 선언(§15.5.6.1)에 나타나는 텍스트 순서로 실행되는 할당 시퀀스에 해당합니다. 부분 클래스 내에서 "텍스트 순서"의 의미는 §15.5.6.1지정됩니다. 정적 생성자(§15.12)가 클래스에 있는 경우 정적 필드 이니셜라이저의 실행은 해당 정적 생성자를 실행하기 직전에 발생합니다. 그렇지 않으면 정적 필드 이니셜라이저는 해당 클래스의 정적 필드를 처음 사용하기 전에 구현 종속 시간에 실행됩니다.

: 예제

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    public static int X = Test.F("Init A");
}

class B
{
    public static int Y = Test.F("Init B");
}

는 다음 출력 중 하나를 생성할 수 있습니다.

Init A
Init B
1 1

또는 결과:

Init B
Init A
1 1

X의 이니셜라이저와 Y의 이니셜라이저 실행은 두 가지 순서로 모두 발생할 수 있습니다; 그들은 해당 필드들에 대한 참조 전에만 발생하도록 제한됩니다. 그러나 예제에서는 다음을 수행합니다.

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    static A() {}
    public static int X = Test.F("Init A");
}

class B
{
    static B() {}
    public static int Y = Test.F("Init B");
}

출력은 다음과 같습니다.

Init B
Init A
1 1

정적 생성자가 실행되는 시점에 대한 규칙(§15.12에 정의된 대로)을 따르면, 의 정적 생성자와 따라서 B의 정적 필드 이니셜라이저는 B의 정적 생성자 및 필드 이니셜라이저보다 먼저 실행되어야 합니다.

끝 예제

15.5.6.3 인스턴스 필드 초기화

클래스의 인스턴스 필드 변수 이니셜라이저는 해당 클래스의 인스턴스 생성자(§15.11.3) 중 하나에 진입할 때 즉시 실행되는 할당 시퀀스에 해당합니다. 부분 클래스 내에서 "텍스트 순서"의 의미는 §15.5.6.1지정됩니다. 변수 이니셜라이저는 클래스 선언(§15.5.6.1)에 나타나는 텍스트 순서로 실행됩니다. 클래스 인스턴스 만들기 및 초기화 프로세스는 §15.11자세히 설명되어 있습니다.

인스턴스 필드의 변수 이니셜라이저는 생성되는 인스턴스를 참조할 수 없습니다. 변수 이니셜라이저가 this을(를) 참조하는 것은 컴파일 시 에러입니다. 왜냐하면 변수 이니셜라이저는 simple_name을 통해 어떤 인스턴스 멤버도 참조할 수 없기 때문입니다.

예제: 다음 코드에서

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

y 변수 이니셜라이저는 생성 중인 인스턴스의 멤버를 참조하므로 컴파일 시간 오류를 발생시킵니다.

끝 예제

15.6 메서드

15.6.1 일반

메서드는 개체 또는 클래스에서 수행할 수 있는 계산이나 작업을 구현하는 멤버입니다. 메서드는 method_declaration사용하여 선언됩니다.

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

문법 참고 사항:

  • unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.
  • method_body를 인식할 때 null_conditional_invocation_expressionexpression 대안이 모두 적용 가능하다면 전자를 선택해야 합니다.

참고: 여기서 대안의 겹침과 우선 순위는 설명적인 편의를 위한 것일 뿐입니다. 겹침을 제거하기 위해 문법 규칙을 자세히 설명할 수 있습니다. ANTLR 및 기타 문법 시스템은 동일한 편의를 채택하므로 method_body 지정된 의미 체계를 자동으로 가합니다. 끝 메모

method_declaration에는 특성 집합 (§22) 및 허용된 종류의 선언된 접근성 (§15.3.6), new(§15.3.5), static(§15.6.3), virtual(§15.6.4), override(§15.6.5), sealed(§15.6.6), abstract(§15.6.7), extern(§15.6.8), 그리고 async(§15.14) 중 하나가 포함될 수 있습니다. 또한 struct_declaration에 직접 포함된 method_declarationreadonly 한정자(§16.4.12)를 포함할 수 있습니다.

다음 중 모두 true인 경우 선언에 유효한 한정자 조합이 있습니다.

  • 선언에는 유효한 액세스 한정자 조합(§15.3.6)이 포함됩니다.
  • 선언에는 동일한 한정자가 여러 번 포함되지 않습니다.
  • 선언에는 다음 한정자 staticvirtualoverride중 최대 하나가 포함됩니다.
  • 선언에는 다음과 같은 한정자 중 하나만 포함됩니다: new 또는 override.
  • 선언에 abstract 한정자가 포함된 경우, 선언에는 다음의 어떤 한정자도 포함되지 않습니다: static, virtual, sealed 또는 extern.
  • 선언에 private 한정자가 포함된 경우, 선언에는 virtual, override, 또는 abstract 한정자가 포함되지 않습니다.
  • 선언에 sealed 한정자가 포함되어 있으면, 선언에는 override 한정자도 포함됩니다.
  • 선언에 partial 한정자가 포함된 경우, new, public, protected, internal, private, virtual, sealed, override, abstract 또는 extern 한정자는 포함되지 않습니다.

메서드는 반환되는 항목(있는 경우)에 따라 분류됩니다.

  • ref가 있는 경우, 메서드는 참조에 의한 반환이며, 선택적으로 읽기 전용인 변수 참조를 반환합니다.
  • 그렇지 않은 경우 return_typevoid 메서드가 값을 반환하지 않음이며 값을 반환하지 않습니다.
  • 다른 경우, 메서드는 값에 의한 반환으로 작동하며, 특정 값을 반환합니다.

값을 반환하는 메서드 선언 또는 값을 반환하지 않는 메서드 선언의 return_type은 메서드가 반환하는 결과의 유형(있는 경우)을 지정합니다. returns-no-value 메서드만 이 한정자(partial)를 포함할 수 있습니다 (§15.6.9). 선언에 async 한정자가 포함된 경우, return_typevoid이어야 하며, 메서드가 값에 의해 반환되면 반환 형식은 작업 형식이어야 합니다 (§15.14.1).

반환 참조 메서드 선언의 ref_return_type은 메서드에서 반환된 variable_reference가 참조하는 변수의 형식을 지정합니다.

제네릭 메서드는 선언에 type_parameter_list 포함하는 메서드입니다. 메서드의 형식 매개 변수를 지정합니다. 선택적 type_parameter_constraints_clause형식 매개 변수에 대한 제약 조건을 지정합니다.

명시적 인터페이스 멤버 구현에 대한 제네릭 method_declaration는 어떤 type_parameter_constraints_clause도 갖지 않아야 합니다. 선언은 인터페이스 메서드의 제약 조건으로부터 모든 제약 조건을 상속받습니다.

마찬가지로 한정자가 있는 override 메서드 선언에는 type_parameter_constraints_clause없으며 메서드 형식 매개 변수의 제약 조건은 재정의되는 가상 메서드에서 상속됩니다.

member_name 메서드의 이름을 지정합니다. 메서드가 명시적 인터페이스 멤버 구현(§18.6.2) 이 아닌 경우 member_name 단순히 식별자입니다.

명시적 인터페이스 멤버 구현의 경우 member_nameinterface_type에 이어 "."와 identifier로 구성됩니다. 이 경우 선언에는 (아마도) extern 또는 async을 제외하고는 다른 수식자를 포함하지 않아야 합니다.

선택적 parameter_list 메서드의 매개 변수를 지정합니다(§15.6.2).

return_type 또는 ref_return_type 및 메서드의 parameter_list에서 참조되는 각 형식은 메서드 자체만큼 (§7.5.5) 액세스할 수 있어야 합니다.

값별 반환 또는 반환 없음 메서드의 method_body 세미콜론, 블록 본문 또는 식 본문입니다. 블록 본문은 메서드가 호출될 때 실행할 문을 지정하는 블록으로 구성됩니다. 식 본문은 => 다음에 null_conditional_invocation_expression 또는 이 오고, 세미콜론으로 구성되며, 메서드가 호출될 때 수행할 단일 식을 나타냅니다.

추상 및 extern 메서드의 경우 method_body 단순히 세미콜론으로 구성됩니다. 부분 메서드의 경우 method_body 세미콜론, 블록 본문 또는 식 본문으로 구성될 수 있습니다. 다른 모든 메서드의 경우 method_body 블록 본문 또는 식 본문입니다.

method_body 세미콜론으로 구성된 경우 선언에는 한정자가 async 포함되지 않습니다.

return-by-ref 메서드의 ref_method_body 세미콜론, 블록 본문 또는 식 본문입니다. 블록 본문은 메서드가 호출될 때 실행할 문을 지정하는 블록으로 구성됩니다. 식 본문은 => 다음에 ref, variable_reference, 세미콜론이 뒤따르며, 메서드가 호출될 때 평가할 단일 variable_reference를 나타냅니다.

추상 및 extern 메서드의 경우 ref_method_body 단순히 세미콜론으로 구성됩니다. 다른 모든 메서드 의 경우 ref_method_body 블록 본문 또는 식 본문입니다.

이름, 형식 매개 변수 수 및 메서드의 매개 변수 목록은 메서드의 시그니처(§7.6)를 정의합니다. 특히 메서드의 서명은 이름, 해당 형식 매개 변수 수 및 숫자, parameter_mode_modifier s(§15.6.2.1) 및 해당 매개 변수 형식으로 구성됩니다. 반환 형식은 메서드의 서명에 속하지 않으며 매개 변수의 이름, 형식 매개 변수의 이름 또는 제약 조건이 아닙니다. 매개 변수 형식이 메서드의 형식 매개 변수를 참조하는 경우 형식 매개 변수의 서수 위치(형식 매개 변수의 이름이 아님)가 형식 동등성에 사용됩니다.

메서드의 이름은 동일한 클래스에 선언된 다른 모든 비 메서드의 이름과 다릅니다. 또한 메서드의 서명은 동일한 클래스에 선언된 다른 모든 메서드의 서명과 달라야 하며, 동일한 클래스에 선언된 두 메서드는 , inout에 의해ref서만 다른 시그니처를 갖지 않습니다.

메서드의 type_parametermethod_declaration 전체 범위 내에서 유효하며, 해당 범위 내의 return_type 또는 ref_return_type, method_body 또는 ref_method_body, 그리고 type_parameter_constraints_clause에서 형식을 구성하는 데 사용할 수 있지만, attributes에서는 사용할 수 없습니다.

모든 매개 변수와 형식 매개 변수의 이름은 서로 다릅니다.

15.6.2 메서드 매개 변수

15.6.2.1 일반

메서드의 매개 변수(있는 경우)는 메서드의 parameter_list 의해 선언됩니다.

parameter_list
    : fixed_parameters
    | fixed_parameters ',' parameter_array
    | parameter_array
    ;

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

매개 변수 목록은 하나 이상의 쉼표로 구분된 매개 변수로 구성되며, 이 중 마지막 매개 변수만이 parameter_array일 수 있습니다.

fixed_parameter 선택적 특성 집합(§22), 선택적in, out, ref또는 this 한정자, 형식, 식별자 및 선택적 default_argument 구성됩니다. 각 fixed_parameter 지정된 이름의 지정된 형식의 매개 변수를 선언합니다. this 한정자는 메서드를 확장 메서드로 지정하고 제네릭이 아닌 중첩되지 않은 정적 클래스에서 정적 메서드의 첫 번째 매개 변수에만 허용됩니다. 매개 변수가 struct 형식이거나 struct로 제한되는 형식 매개 변수인 경우, this 한정자는 ref 또는 in 한정자와 결합될 수 있지만, out 한정자와는 결합할 수 없습니다. 확장 메서드는 §15.6.10자세히 설명되어 있습니다. fixed_parameterdefault_argument이 있으면 선택적 매개 변수로 알려져 있으며, fixed_parameterdefault_argument이 없으면 필수 매개 변수입니다. 필수 매개 변수는 parameter_list 선택적 매개 변수 후에 나타나지 않습니다.

ref, out 또는 this 한정자가 있는 매개변수는 default_argument를 가질 수 없습니다. 입력 매개 변수에 default_argument 있을 수 있습니다. default_argument에서 다음 중 하나여야 합니다.

  • a constant_expression
  • 값 형식인 new S()의 형태인 S의 표현입니다.
  • 값 형식인 default(S)의 형태인 S의 표현입니다.

매개 변수의 형식에 ID 변환 또는 nullable 변환을 통해 암시적으로 변환될 수 있어야 합니다.

구현 부분 메서드 선언(§15.6.9)에서 선택적 매개 변수가 발생하는 경우 명시적 인터페이스 멤버 구현(§18.6.2), 단일 매개 변수 인덱서 선언(§) 15.9) 또는 연산자 선언(§15.10.1)에서는 이러한 멤버를 인수를 생략할 수 있는 방식으로 호출할 수 없으므로 컴파일러에서 경고를 제공해야 합니다.

파라미터 배열은 선택적 속성들(§22), params, 배열 유형, 및 식별자로 구성됩니다. 매개 변수 배열은 지정된 이름의 지정된 배열 형식의 단일 매개 변수를 선언합니다. 매개 변수 배열의 array_type 1차원 배열 형식(§17.2)이어야 합니다. 메서드 호출에서 매개 변수 배열은 지정된 배열 형식의 단일 인수를 지정하거나 배열 요소 형식의 인수를 0개 이상 지정할 수 있도록 허용합니다. 매개 변수 배열은 §15.6.2.4자세히 설명되어 있습니다.

parameter_array 선택적 매개 변수 후에 발생할 수 있지만 기본값을 가질 수 없습니다. parameter_array 인수를 생략하면 빈 배열이 생성됩니다.

예제: 다음은 다양한 종류의 매개 변수를 보여 줍니다.

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

의 매개변수 목록에서, 는 필수 매개변수이고, 는 필수 값 매개변수이며, , , 는 선택적 값 매개변수이고, 는 매개변수 배열입니다.

끝 예제

메서드 선언은 매개 변수 및 형식 매개 변수에 대해 별도의 선언 공간(§7.3)을 만듭니다. 이름은 형식 매개 변수 목록과 메서드의 매개 변수 목록을 통해 이 선언 공간에 도입됩니다. 메서드의 본문(있는 경우)은 이 선언 공간 내에 중첩된 것으로 간주됩니다. 메서드 선언 공간의 두 멤버가 같은 이름을 갖는 것은 오류입니다.

메서드 호출(§12.8.10.2)은 메서드의 매개 변수 및 지역 변수에 대한 해당 호출과 관련된 복사본을 만들고 호출의 인수 목록은 새로 만든 매개 변수에 값 또는 변수 참조를 할당합니다. 메서드 블록 내에서 매개 변수는 simple_name 식(§12.8.4)의 식별자에서 참조할 수 있습니다.

다음과 같은 종류의 매개 변수가 있습니다.

참고: §7.6설명된 대로 , inout 한정자는 메서드 서명의 일부이지만 ref 한정자는 그렇지 않습니다. 끝 메모

15.6.2.2 값 매개 변수

한정자 없이 선언된 매개 변수는 값 매개 변수입니다. 값 매개 변수는 메서드 호출에 제공된 해당 인수에서 초기 값을 가져오는 지역 변수입니다.

명확한 할당 규칙은 §9.2.5를 참조하세요.

메서드 호출의 해당 인수는 매개 변수 형식으로 암시적으로 변환할 수 있는 식(§10.2)이어야 합니다.

메서드는 값 매개 변수에 새 값을 할당할 수 있습니다. 이러한 할당은 값 매개 변수가 나타내는 로컬 스토리지 위치에만 영향을 줍니다. 메서드 호출에 지정된 실제 인수에는 영향을 주지 않습니다.

15.6.2.3 참조 매개 변수

15.6.2.3.1 일반

입력, 출력 및 참조 매개 변수는 참조 매개 변수입니다. 참조 기준 매개 변수는 로컬 참조 변수(§9.7)이며, 초기 참조는 메서드 호출에 제공된 해당 인수에서 가져옵니다.

참고: 참조 매개 변수 참조는 ref assignment(= ref) 연산자를 사용하여 변경할 수 있습니다.

매개 변수가 참조 매개 변수인 경우, 메서드 호출 시 해당 인수는 in, ref, 또는 out와 같은 해당 키워드 뒤에 매개 변수와 동일한 형식의 변수 참조 (§9.5)로 구성됩니다. 그러나 매개 변수가 in매개 변수인 경우, 인수는 해당 인수 식으로부터 해당 매개 변수의 형식으로의 암시적 변환(§10.2)이 존재하는 일 수 있습니다.

반복기(§15.15) 또는 비동기 함수(§15.14)로 선언된 함수에는 참조별 매개 변수가 허용되지 않습니다.

여러 참조 매개 변수를 사용하는 메서드에서는 여러 이름이 동일한 스토리지 위치를 나타낼 수 있습니다.

15.6.2.3.2 입력 매개 변수

한정자를 사용하여 in 선언된 매개 변수는 입력 매개 변수입니다. 입력 매개 변수에 해당하는 인수는 메서드 호출 시점에 존재하는 변수이거나 메서드 호출에서 구현(§12.6.2.3)에 의해 생성된 변수입니다. 명확한 할당 규칙은 §9.2.8을 참조하세요.

입력 매개 변수의 값을 수정하는 것은 컴파일 시간 오류입니다.

참고: 입력 매개 변수의 주요 목적은 효율성을 위한 것입니다. 메서드 매개 변수의 형식이 메모리 요구 사항 측면에서 큰 구조체인 경우 메서드를 호출할 때 인수의 전체 값을 복사하지 않도록 하는 것이 유용합니다. 입력 매개 변수를 사용하면 메서드가 메모리의 기존 값을 참조하는 동시에 해당 값에 대한 원치 않는 변경에 대한 보호를 제공할 수 있습니다. 끝 메모

15.6.2.3.3 참조 매개 변수

한정자를 사용하여 ref 선언된 매개 변수는 참조 매개 변수입니다. 명확한 할당 규칙은 §9.2.6을 참조하세요.

: 예제

class Test
{
    static void Swap(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main()
    {
        int i = 1, j = 2;
        Swap(ref i, ref j);
        Console.WriteLine($"i = {i}, j = {j}");
    }
}

는 출력을 생성합니다.

i = 2, j = 1

Swap에서 Main의 호출의 경우, xi을 나타내고 yj을 나타냅니다. 따라서 호출은 ij의 값을 서로 교환하는 효과가 있습니다.

끝 예제

예제: 다음 코드에서

class A
{
    string s;
    void F(ref string a, ref string b)
    {
        s = "One";
        a = "Two";
        b = "Three";
    }

    void G()
    {
        F(ref s, ref s);
    }
}

FG 호출은 s에 대한 참조를 ab 모두에 전달합니다. 따라서 해당 호출의 경우 이름 sab 모든 항목은 동일한 스토리지 위치를 참조하며, 세 가지 할당은 모두 인스턴스 필드를 s수정합니다.

끝 예제

struct 유형의 경우, 인스턴스 메서드, 인스턴스 접근자(§12.2.1), 또는 생성자 이니셜라이저가 있는 인스턴스 생성자 내에서 this 키워드는 구조체 형식(§12.8.14)의 참조 매개 변수처럼 정확하게 동작합니다.

15.6.2.3.4 출력 매개 변수

한정자를 사용하여 out 선언된 매개 변수는 출력 매개 변수입니다. 명확한 할당 규칙은 §9.2.7을 참조하세요.

부분 메서드(§15.6.9)로 선언된 메서드에는 출력 매개 변수가 없습니다.

참고: 출력 매개 변수는 일반적으로 여러 반환 값을 생성하는 메서드에서 사용됩니다. 끝 메모

예제:

class Test
{
    static void SplitPath(string path, out string dir, out string name)
    {
        int i = path.Length;
        while (i > 0)
        {
            char ch = path[i - 1];
            if (ch == '\\' || ch == '/' || ch == ':')
            {
                break;
            }
            i--;
        }
        dir = path.Substring(0, i);
        name = path.Substring(i);
    }

    static void Main()
    {
        string dir, name;
        SplitPath(@"c:\Windows\System\hello.txt", out dir, out name);
        Console.WriteLine(dir);
        Console.WriteLine(name);
    }
}

이 예제에서는 출력을 생성합니다.

c:\Windows\System\
hello.txt

dir 변수 및 name 변수는 전달SplitPath되기 전에 할당되지 않을 수 있으며 호출 후에는 확실히 할당된 것으로 간주됩니다.

끝 예제

15.6.2.4 매개 변수 배열

한정자로 선언된 params 매개 변수는 매개 변수 배열입니다. 매개 변수 목록에 매개 변수 배열이 포함된 경우 이 매개 변수는 목록의 마지막 매개 변수여야 하며 1차원 배열 형식이어야 합니다.

: 형식 string[] 이며 string[][] 매개 변수 배열의 형식으로 사용할 수 있지만 형식 string[,] 은 사용할 수 없습니다. 끝 예제

참고: params 수정자를 in, out, 또는 ref 수정자와 결합할 수 없습니다. 끝 메모

매개 변수 배열은 메서드 호출의 두 가지 방법 중 하나로 인수를 지정할 수 있도록 허용합니다.

  • 매개 변수 배열에 지정된 인수는 매개 변수 배열 형식으로 암시적으로 변환할 수 있는 단일 식(§10.2)일 수 있습니다. 이 경우 매개 변수 배열은 값 매개 변수처럼 정확하게 작동합니다.
  • 또는 호출에서 매개 변수 배열에 대해 0개 이상의 인수를 지정할 수 있습니다. 여기서 각 인수는 매개 변수 배열의 요소 형식으로 암시적으로 변환할 수 있는 식(§10.2)입니다. 이 경우 호출은 인수 수에 해당하는 길이가 있는 매개 변수 배열 형식의 인스턴스를 만들고, 지정된 인수 값을 사용하여 배열 인스턴스의 요소를 초기화하고, 새로 만든 배열 인스턴스를 실제 인수로 사용합니다.

호출에서 가변 개수의 인수를 허용하는 경우를 제외하고 매개 변수 배열은 동일한 형식의 값 매개 변수(§15.6.2.2)와 정확히 동일합니다.

: 예제

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {i}");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        int[] arr = {1, 2, 3};
        F(arr);
        F(10, 20, 30, 40);
        F();
    }
}

는 출력을 생성합니다.

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

첫 번째 호출 F 은 단순히 배열 arr 을 값 매개 변수로 전달합니다. F의 두 번째 호출은 지정된 요소 값이 있는 4개 요소를 int[] 자동으로 만들고 해당 배열 인스턴스를 값 매개 변수로 전달합니다. 마찬가지로 세 번째 호출 F 은 0 요소를 int[] 만들고 해당 인스턴스를 값 매개 변수로 전달합니다. 두 번째 및 세 번째 호출은 쓰기와 정확히 동일합니다.

F(new int[] {10, 20, 30, 40});
F(new int[] {});

끝 예제

오버로드 확인을 수행할 때 매개 변수 배열이 있는 메서드는 일반 형식 또는 확장된 형식(§12.6.4.2)에서 적용할 수 있습니다. 메서드의 확장된 형식은 메서드의 일반 형식을 적용할 수 없는 경우에만 사용할 수 있으며 확장된 양식과 서명이 같은 해당 메서드가 동일한 형식으로 선언되지 않은 경우에만 사용할 수 있습니다.

: 예제

class Test
{
    static void F(params object[] a) =>
        Console.WriteLine("F(object[])");

    static void F() =>
        Console.WriteLine("F()");

    static void F(object a0, object a1) =>
        Console.WriteLine("F(object,object)");

    static void Main()
    {
        F();
        F(1);
        F(1, 2);
        F(1, 2, 3);
        F(1, 2, 3, 4);
    }
}

는 출력을 생성합니다.

F()
F(object[])
F(object,object)
F(object[])
F(object[])

이 예제에서는 매개 변수 배열이 있는 메서드의 확장 가능한 두 가지 형태가 이미 클래스에 일반 메서드로 포함되어 있습니다. 따라서 이러한 확장된 양식은 오버로드 확인을 수행할 때 고려되지 않으며, 따라서 첫 번째 및 세 번째 메서드 호출은 일반 메서드를 선택합니다. 클래스가 매개 변수 배열을 사용하여 메서드를 선언하는 경우 일부 확장된 폼을 일반 메서드로 포함하는 것은 드문 일이 아닙니다. 이렇게 하면 매개 변수 배열이 있는 메서드의 확장된 형식이 호출될 때 발생하는 배열 인스턴스의 할당을 방지할 수 있습니다.

끝 예제

배열은 참조 형식이므로 매개 변수 배열에 대해 전달된 값이 될 null수 있습니다.

: 예제:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

결과를 생성합니다.

True
False

두 번째 호출은 False를 생성하며 이는 F(new string[] { null })와 동일하고 단일 null 참조를 포함하는 배열을 전달합니다.

끝 예제

매개 변수 배열의 형식이면 object[]메서드의 정규 형식과 단일 object 매개 변수의 확장된 형식 간에 모호성이 발생할 수 있습니다. 모호성의 이유는 object[] 자체가 암시적으로 형식 object으로 변환될 수 있기 때문입니다. 그러나 필요한 경우 캐스트를 삽입하여 해결할 수 있으므로 모호성은 문제가 되지 않습니다.

: 예제

class Test
{
    static void F(params object[] args)
    {
        foreach (object o in args)
        {
            Console.Write(o.GetType().FullName);
            Console.Write(" ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        object[] a = {1, "Hello", 123.456};
        object o = a;
        F(a);
        F((object)a);
        F(o);
        F((object[])o);
    }
}

는 출력을 생성합니다.

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

첫 번째 및 마지막 호출F에서는 인수 형식에서 매개 변수 형식(둘 다 형식F)으로 암시적 변환이 존재하기 때문에 일반적인 형식 object[] 을 적용할 수 있습니다. 따라서 오버로드 확인은 일반 형식을 F선택하고 인수는 일반 값 매개 변수로 전달됩니다. 두 번째 및 세 번째 호출에서는 인수 유형에서 매개변수 유형으로의 암시적 변환이 없기 때문에 F의 일반 형식을 적용할 수 없습니다 (즉, 유형 object를 유형 object[]로 암시적으로 변환할 수 없습니다). 그러나 확장된 형식 F을 적용할 수 있으므로 오버로드 결정에 의해 선택됩니다. 결과적으로 호출을 통해 한 요소가 object[] 만들어지고 배열의 단일 요소가 지정된 인수 값(그 자체가 참조 object[])을 사용하여 초기화됩니다.

끝 예제

15.6.3 정적 및 인스턴스 메서드

메서드 선언에 한 static 정자가 포함된 경우 해당 메서드는 정적 메서드라고 합니다. 한정자가 없 static 으면 메서드를 인스턴스 메서드라고 합니다.

정적 메서드는 특정 인스턴스에서 작동하지 않습니다. 정적 메서드에서 this를 참조하는 것은 컴파일 시간 오류입니다.

인스턴스 메서드는 클래스의 지정된 인스턴스에서 작동하며 해당 인스턴스는 this(§12.8.14)로 액세스할 수 있습니다.

정적 멤버와 인스턴스 멤버의 차이점은 §15.3.8에서 자세히 설명합니다.

15.6.4 가상 메서드

인스턴스 메서드 선언에 가상 한정자가 포함된 경우 해당 메서드는 가상 메서드라고 합니다. 가상 한정자가 없는 경우 메서드는 가상이 아닌 메서드라고 합니다.

가상이 아닌 메서드의 구현은 고정됩니다. 구현은 메서드가 선언된 클래스의 인스턴스 또는 파생 클래스의 인스턴스에서 호출되는지 여부에 관계없이 동일합니다. 반면, 가상 메서드의 구현은 파생 클래스로 대체될 수 있습니다. 상속된 가상 메서드의 구현을 대체하는 프로세스를 해당 메서드를 재정의하는 것으로 알려져 있습니다(§15.6.5).

가상 메서드 호출 에서 해당 호출이 수행되는 인스턴스의 런타임 형식 은 호출할 실제 메서드 구현을 결정합니다. 가상이 아닌 메서드 호출에서 인스턴스의 컴파일 시간 형식 이 결정 요소입니다. 정확히 말하자면, 컴파일 시간 형식이 N이고 런타임 형식이 A인 인스턴스에서, CR 또는 R로부터 파생된 클래스 중 하나일 때, C인수 목록을 사용하여 명명된 C 메서드를 호출하면 호출이 다음과 같이 처리됩니다.

  • 바인딩 시 오버로드 해석은 C, N, A에 적용되어 M에서 선언되거나 상속된 메서드 집합 중 특정 메서드 C를 선택합니다. 이 내용은 §12.8.10.2에 설명되어 있습니다.
  • 그런 다음 런타임에 다음을 수행합니다.
    • 가상이 아닌 메서드 M 인 경우 M 호출됩니다.
    • 그렇지 않으면 M는 가상 메서드로, M와 관련하여 R의 가장 하위의 구현이 호출됩니다.

클래스에서 선언되거나 클래스에서 상속된 모든 가상 메서드에 대해 해당 클래스와 관련하여 메서드의 가장 파생된 구현이 있습니다. 클래스 M 와 관련하여 가상 메서드 R 의 가장 파생된 구현은 다음과 같이 결정됩니다.

  • RM의 도입 가상 선언을 포함한다면, 이는 M에 대한 R의 가장 파생된 구현입니다.
  • 만약 RM를 재정의한다면, 이 경우 M에 대한 R의 가장 구체적인 구현입니다.
  • 그렇지 않으면 M과 관련하여 R의 가장 파생된 구현은, M의 직접 기본 클래스와 관련하여 R의 가장 파생된 구현과 동일합니다.

예제: 다음 예제에서는 가상 메서드와 가상이 아닌 메서드 간의 차이점을 보여 줍니다.

class A
{
    public void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public new void F() => Console.WriteLine("B.F");
    public override void G() => Console.WriteLine("B.G");
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        a.F();
        b.F();
        a.G();
        b.G();
    }
}

이 예제 A 에서는 가상이 아닌 메서드 F 와 가상 메서드 G를 소개합니다. 이 클래스 B는 새 비가상 메서드F를 도입하여 상속된 F, 또한 상속된 메서드G. 이 예제에서는 출력을 생성합니다.

A.F
B.F
B.G
B.G

a.G()문은 B.G가 아니라 A.G을(를) 호출합니다. 이는 인스턴스의 컴파일 시간 형식이 아닌 인스턴스의 런타임 형식(즉,)이 호출할 실제 메서드 구현을 결정하기 때문입니다BA.

끝 예제

메서드는 상속된 메서드를 숨길 수 있으므로 클래스에 동일한 서명이 있는 여러 가상 메서드를 포함할 수 있습니다. 가장 파생된 메서드를 제외한 모든 메서드가 숨겨져 있으므로 모호성 문제가 발생하지 않습니다.

예제: 다음 코드에서

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new virtual void F() => Console.WriteLine("C.F");
}

class D : C
{
    public override void F() => Console.WriteLine("D.F");
}

class Test
{
    static void Main()
    {
        D d = new D();
        A a = d;
        B b = d;
        C c = d;
        a.F();
        b.F();
        c.F();
        d.F();
    }
}

CD 클래스에는 동일한 서명을 가진 두 개의 가상 메서드가 포함됩니다. 하나는 A에 의해 도입되었고 다른 하나는 C에 의해 도입되었습니다. 에 의해 C 도입된 메서드는 상속된 A메서드를 숨깁니다. 따라서 D에서의 재정의 선언은 C에 의해 도입된 메서드를 재정의하며, DA에 의해 도입된 메서드를 재정의할 수 없습니다. 이 예제에서는 출력을 생성합니다.

B.F
B.F
D.F
D.F

메서드가 숨겨지지 않은 덜 파생된 형식을 통해 인스턴스 D 에 액세스하여 숨겨진 가상 메서드를 호출할 수 있습니다.

끝 예제

15.6.5 메서드 재정의

인스턴스 메서드 선언에 한 override 정자가 포함된 경우 메서드는 재정의 메서드라고 합니다. 재정의 메서드는 동일한 서명을 사용하여 상속된 가상 메서드를 재정의합니다. 가상 메서드 선언 은 새 메서드를 도입하는 반면, 재정의 메서드 선언 해당 메서드의 새 구현을 제공하여 기존 상속된 가상 메서드를 특수화합니다.

재정의 선언에 의해 재정의된 메서드는 재정의된 기본 메서드라고 합니다. 클래스에서 선언된 재정의 메서드의 경우, 재정의된 기본 메서드는 각 기본 클래스를 검사하여 결정됩니다. 이는 지정된 기본 클래스 형식에서 형식 인수를 대체한 후, 동일한 시그니처를 가진 접근 가능한 메서드를 찾을 때까지 직접 상위 클래스부터 시작하여 연속적으로 각 직접 상위 클래스를 검사하는 방식으로 진행됩니다. 재정의된 기본 메서드를 찾기 위해 메서드는 public가 있는 경우, protected가 있는 경우, protected internal가 있는 경우, 또는 internal 또는 private protected가 같은 프로그램에서 C로 선언된 경우에 액세스할 수 있는 것으로 간주됩니다.

재정의 선언에 대해 다음 조건이 모두 true일 경우가 아니라면 컴파일 시간 오류가 발생합니다.

  • 재정의된 기본 메서드는 위에서 설명한 대로 찾을 수 있습니다.
  • 이러한 재정의된 기본 메서드는 정확히 하나 있습니다. 이 제한은 기본 클래스 형식이 생성된 형식인 경우에만 적용됩니다. 여기서 형식 인수를 대체하면 두 메서드의 시그니처가 동일합니다.
  • 재정의된 기본 메서드는 가상 메서드, 추상 메서드 또는 오버라이드 메서드입니다. 즉, 재정의된 기본 메서드는 정적 또는 가상이 아닌 메서드일 수 없습니다.
  • 재정의된 기본 메서드는 봉인된 메서드가 아닙니다.
  • 재정의된 기본 메서드의 반환 형식과 재정의 메서드 간에 동일성 변환이 있습니다.
  • 재정의 선언과 재정의된 기본 메서드는 동일한 명시된 접근성을 갖습니다. 즉, 오버라이드 선언은 가상 메서드의 접근성을 변경할 수 없습니다. 그러나 재정의된 기본 메서드가 내부적으로 보호되고 재정의 선언을 포함하는 어셈블리와 다른 어셈블리에 선언된 경우 재정의 선언의 선언된 접근성이 보호됩니다.
  • 재정의 선언은 type_parameter_constraints_clause를 지정하지 않습니다. 대신 제약 조건은 재정의된 기본 메서드에서 상속됩니다. 재정의된 메서드의 형식 매개 변수인 제약 조건은 상속된 제약 조건의 형식 인수로 대체될 수 있습니다. 이렇게 하면 값 형식 또는 봉인된 형식과 같이 명시적으로 지정할 때 유효하지 않은 제약 조건이 발생할 수 있습니다.

예제: 다음은 제네릭 클래스에 대해 재정의 규칙이 작동하는 방식을 보여 줍니다.

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

끝 예제

재정의 선언은 base_access (§12.8.15)를 사용하여 재정의된 기본 메서드에 액세스할 수 있습니다.

예제: 다음 코드에서

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

base.PrintFields()에서 BA에 선언된 PrintFields 메서드를 호출합니다. base_access 가상 호출 메커니즘을 사용하지 않도록 설정하고 기본 메서드를 비 메서드virtual로 처리합니다. 가상이고 B의 런타임 유형이 ((A)this).PrintFields()이기 때문에 PrintFieldsB로 작성되어 있었다면, A에 선언된 메서드가 아니라 PrintFields에 선언된 ((A)this) 메서드를 재귀적으로 호출했을 것입니다.

끝 예제

한정자를 override 포함해야만 메서드가 다른 메서드를 재정의할 수 있습니다. 다른 모든 경우에서 상속된 메서드와 시그니처가 같은 메서드는 상속된 메서드를 숨기기만 하면 됩니다.

예제: 다음 코드에서

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

F 안의 B 메서드는 override 한정자를 포함하지 않으므로 F 안의 A 메서드를 재정의하지 않습니다. 오히려 FB 메서드가 A의 메서드를 숨기며, 이 선언에는 새 한정자가 포함되지 않았기 때문에 경고가 보고됩니다.

끝 예제

예제: 다음 코드에서

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

F의 메서드는 B에서 F로부터 상속된 가상 A 메서드를 숨깁니다. new F in B 에는 프라이빗 액세스 권한이 있으므로 해당 범위에는 클래스 본문 B 만 포함되며 확장 C되지 않습니다. 따라서 F에서 C의 선언은 F로부터 상속된 A를 재정의할 수 있습니다.

끝 예제

15.6.6 봉인된 메서드

인스턴스 메서드 선언에 한 sealed 정자가 포함된 경우 해당 메서드는 봉인된 메서드라고 합니다. 봉인된 메서드는 동일한 서명을 가진 상속된 가상 메서드를 재정의합니다. 봉인된 메서드도 한정자를 사용하여 override 표시해야 합니다. 한정자를 사용하면 파생 클래스가 메서드를 추가로 재정의 sealed 할 수 없게 됩니다.

: 예제

class A
{
    public virtual void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public sealed override void F() => Console.WriteLine("B.F");
    public override void G()        => Console.WriteLine("B.G");
}

class C : B
{
    public override void G() => Console.WriteLine("C.G");
}

클래스 B는 두 가지 재정의 메서드를 제공합니다: F 수식자가 있는 sealed 메서드와 F 수식자가 없는 메서드입니다. Bsealed 한정자를 사용함으로써 CF을(를) 더 이상 재정의하지 못하게 합니다.

끝 예제

15.6.7 추상 메서드

인스턴스 메서드 선언에 한 abstract 정자가 포함된 경우 해당 메서드는 추상 메서드라고 합니다. 추상 메서드는 암시적으로도 가상 메서드이지만 한정자를 virtual가질 수 없습니다.

추상 메서드 선언은 새 가상 메서드를 도입하지만 해당 메서드의 구현을 제공하지는 않습니다. 대신 비 추상 파생 클래스는 해당 메서드를 재정의하여 고유한 구현을 제공해야 합니다. 추상 메서드는 실제 구현을 제공하지 않으므로 추상 메서드의 메서드 본문은 단순히 세미콜론으로 구성됩니다.

추상 메서드 선언은 추상 클래스(§15.2.2.2)에서만 허용됩니다.

예제: 다음 코드에서

public abstract class Shape
{
    public abstract void Paint(Graphics g, Rectangle r);
}

public class Ellipse : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r);
}

public class Box : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r);
}

클래스는 Shape 자신을 그릴 수 있는 기하 도형 개체의 추상 개념을 정의합니다. Paint 의미 있는 기본 구현이 없으므로 메서드는 추상적입니다. Ellipse 클래스 및 Box 클래스는 구체적인 Shape 구현입니다. 이러한 클래스는 추상이 아니기 때문에 Paint 메서드를 재정의하고 실제 구현을 제공해야 합니다.

끝 예제

base_access(§12.8.15)가 추상 메서드를 참조하는 것은 컴파일 시간 오류입니다.

예제: 다음 코드에서

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

컴파일 시 오류가 발생하는 이유는 base.F() 호출이 추상 메서드를 참조하기 때문입니다.

끝 예제

추상 메서드 선언은 가상 메서드를 재정의할 수 있습니다. 이렇게 하면 추상 클래스가 파생 클래스에서 메서드를 강제로 다시 구현할 수 있으며 메서드의 원래 구현을 사용할 수 없게 됩니다.

예제: 다음 코드에서

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

abstract class B: A
{
    public abstract override void F();
}

class C : B
{
    public override void F() => Console.WriteLine("C.F");
}

클래스 A 는 가상 메서드를 선언하고, 클래스 B 는 추상 메서드로 이 메서드를 재정의하고, 클래스 C 는 추상 메서드를 재정의하여 자체 구현을 제공합니다.

끝 예제

15.6.8 외부 메서드

메서드 선언에 한 extern 정자가 포함된 경우 메서드는 외부 메서드라고 합니다. 외부 메서드는 일반적으로 C#이 아닌 언어를 사용하여 외부에서 구현됩니다. 외부 메서드 선언은 실제 구현을 제공하지 않으므로 외부 메서드의 메서드 본문은 단순히 세미콜론으로 구성됩니다. 외부 메서드는 제네릭이 아니어야 합니다.

외부 메서드에 대한 연결이 수행되는 메커니즘은 구현에서 정의됩니다.

예제: 다음 예제에서는 extern 수정자와 DllImport 속성을 사용하는 방법을 보여 줍니다.

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

끝 예제

15.6.9 부분 메서드

메서드 선언에 한 partial 정자가 포함된 경우 해당 메서드는 부분 메서드라고 합니다. Partial 메서드는 partial 형식(§15.2.7)의 멤버로만 선언할 수 있으며 여러 제한 사항이 적용됩니다.

부분 메서드는 형식 선언의 한 부분에서 정의되고 다른 부분에서 구현될 수 있습니다. 구현은 선택 사항입니다. 부분 메서드를 구현하는 파트가 없으면 부분 메서드 선언과 해당 부분의 모든 호출이 부분의 조합으로 인해 형식 선언에서 제거됩니다.

부분 메서드는 액세스 한정자를 정의하지 않습니다. 암시적으로 비공개입니다. 반환 형식은 void출력 매개 변수가 아니어야 합니다. partial 키워드 바로 앞에 나타나는 경우에만 메서드 선언에서 식별자 컨텍스트 키워드(void)로 인식됩니다. 부분 메서드는 인터페이스 메서드를 명시적으로 구현할 수 없습니다.

부분 메서드 선언에는 두 가지 종류가 있습니다. 메서드 선언의 본문이 세미콜론인 경우 선언은 부분 메서드 선언을 정의하는 것이라고 합니다. 본문이 세미콜론이 아닌 경우 선언은 구현 부분 메서드 선언이라고 합니다. 형식 선언의 일부에는 지정된 서명이 있는 부분 메서드 선언을 정의하는 것이 하나만 있어야 하며, 지정된 서명이 있는 부분 메서드 선언을 구현하는 것은 하나만 있어야 합니다. 구현 부분 메서드 선언이 지정된 경우 해당 부분 메서드 선언을 정의해야 하며 선언은 다음에 지정된 대로 일치해야 합니다.

  • 선언에는 동일한 한정자(반드시 같은 순서는 아니지만), 메서드 이름, 형식 매개 변수 수 및 매개 변수 수가 있어야 합니다.
  • 선언의 해당 매개 변수에는 동일한 한정자(반드시 동일한 순서는 아니지만)와 동일한 형식 또는 ID 변환 가능 형식(형식 매개 변수 이름의 모듈로 차이)이 있어야 합니다.
  • 선언의 해당 형식 매개 변수는 동일한 제약 조건(형식 매개 변수 이름의 모듈로 차이)을 가져야 합니다.

부분 메서드 선언 구현은 해당 부분 메서드 선언을 정의하는 부분 메서드 선언과 동일한 부분에 나타날 수 있습니다.

부분 메서드 정의만 오버로드 해석에 참여합니다. 따라서 구현 선언이 제공되었는지 여부에 관계없이 호출 식은 부분 메서드의 호출로 확인될 수 있습니다. partial 메서드는 항상 void를 반환하기 때문에 이러한 호출 식은 항상 식 문입니다. 또한 partial 메서드는 암시적으로 private있기 때문에 해당 문은 항상 partial 메서드가 선언되는 형식 선언의 일부 내에서 발생합니다.

참고: 부분 메서드 선언을 정의하고 구현하는 일치 정의에서는 매개 변수 이름을 일치시킬 필요가 없습니다. 이것은 명명된 인수(§12.6.2.1)가 사용될 때 놀랍지만 잘 정의된 동작을 생성할 수 있습니다. 예를 들어, 한 파일에서 부분 메서드 선언을 정의하고 다른 파일에서 부분 메서드 선언을 구현하는 경우가 있습니다.

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

호출이 부분 메서드 선언을 정의하는 것이 아니라 구현의 인수 이름을 사용하기 때문에 유효 하지 않습니다.

끝 메모

partial 형식 선언의 일부가 지정된 partial 메서드에 대한 구현 선언을 포함하지 않는 경우 이를 호출하는 식 문은 결합된 형식 선언에서 단순히 제거됩니다. 따라서 하위 식이 포함된 호출 식은 런타임에 영향을 주지 않습니다. 부분 메서드 자체도 제거되며 결합된 형식 선언의 멤버가 되지 않습니다.

지정된 partial 메서드에 대한 구현 선언이 있는 경우 partial 메서드의 호출은 유지됩니다. 부분 메서드는 다음을 제외하고, 구현되는 부분 메서드 선언과 유사한 메서드 선언을 유발합니다.

  • partial 수정자는 포함되지 않습니다.

  • 결과 메서드 선언의 특성은 정의의 결합된 특성과 지정되지 않은 순서로 부분 메서드 선언을 구현하는 특성입니다. 중복 항목은 제거되지 않습니다.

  • 결과 메서드 선언의 매개 변수에 대한 특성은 정의에 해당하는 매개 변수의 결합된 특성과 지정되지 않은 순서로 부분 메서드 선언을 구현하는 특성입니다. 중복 항목은 제거되지 않습니다.

부분 메서드 M에 대해 구현 선언이 아닌 정의 선언을 지정하는 경우 다음 제한 사항이 적용됩니다.

  • M(§12.8.17.5)에서 대리자를 만드는 것은 컴파일 시간 오류입니다.

  • 컴파일 시간 오류는 식 트리 형식(M)으로 변환되는 익명 함수 내에서 를 참조하는 것입니다.

  • 호출 M 내의 일부로 발생하는 식은 확정 할당 상태(§9.4)에 영향을 미치지 않으며, 이는 컴파일 시간 오류로 이어질 수 있습니다.

  • M 애플리케이션의 진입점이 될 수 없습니다(§7.1).

부분 메서드는 형식 선언의 한 부분이 다른 부분(예: 도구에서 생성되는 부분)의 동작을 사용자 지정하도록 허용하는 데 유용합니다. 다음 부분 클래스 선언을 고려합니다.

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

이 클래스가 다른 부분 없이 컴파일되는 경우 부분 메서드 선언과 해당 호출을 정의하는 것이 제거되고 결과 결합 클래스 선언은 다음과 같습니다.

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

그러나 부분 메서드의 구현 선언을 제공하는 다른 부분이 있다고 가정합니다.

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

그런 다음 결과 결합된 클래스 선언은 다음과 같습니다.

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

15.6.10 확장 메서드

메서드의 첫 번째 매개 변수에 한정자가 포함된 this 경우 해당 메서드는 확장 메서드라고 합니다. 확장 메서드는 제네릭이 아닌 중첩되지 않은 정적 클래스에서만 선언되어야 합니다. 확장 메서드의 첫 번째 매개 변수는 다음과 같이 제한됩니다.

  • 값 형식이 있는 경우에만 입력 매개 변수일 수 있습니다.
  • 값 형식이 있거나 구조체로 제한되는 제네릭 형식이 있는 경우에만 참조 매개 변수일 수 있습니다.
  • 포인터 형식이 아니어야 합니다.

예제: 다음은 두 개의 확장 메서드를 선언하는 정적 클래스의 예입니다.

public static class Extensions
{
    public static int ToInt32(this string s) => Int32.Parse(s);

    public static T[] Slice<T>(this T[] source, int index, int count)
    {
        if (index < 0 || count < 0 || source.Length - index < count)
        {
            throw new ArgumentException();
        }
        T[] result = new T[count];
        Array.Copy(source, index, result, 0, count);
        return result;
    }
}

끝 예제

확장 메서드는 일반 정적 메서드입니다. 또한 둘러싸는 정적 클래스가 범위에 있는 경우, 수신기 식을 첫 번째 인수로 사용하여 인스턴스 메서드 호출 구문(§12.8.10.3)을 통해 확장 메서드를 호출할 수 있습니다.

: 다음 프로그램은 위에 선언된 확장 메서드를 사용합니다.

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

Slice 메서드는 string[]에서 사용할 수 있으며, ToInt32 메서드는 string에서 사용할 수 있습니다. 두 메서드는 확장 메서드로 선언되었습니다. 프로그램의 의미는 일반적인 정적 메서드 호출을 사용하여 다음과 같습니다.

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in Extensions.Slice(strings, 1, 2))
        {
            Console.WriteLine(Extensions.ToInt32(s));
        }
    }
}

끝 예제

15.6.11 메서드 본문

메서드 선언의 메서드 본문은 블록 본문, 식 본문 또는 세미콜론으로 구성됩니다.

추상 및 외부 메서드 선언은 메서드 구현을 제공하지 않으므로 메서드 본문은 단순히 세미콜론으로 구성됩니다. 다른 메서드의 경우 메서드 본문은 해당 메서드가 호출될 때 실행할 문이 포함된 블록(§13.3)입니다.

메서드의 유효 반환 형식void 반환 형식이거나 메서드가 비동기이고 반환 형식이 (void)인 경우입니다 «TaskType» . 그렇지 않으면 비동기 메서드의 유효 반환 형식은 반환 형식이며 반환 형식이 있는 비동기 메서드의 유효 반환 형식(«TaskType»<T>)은 반환 형식입니다T.

메서드의 유효 반환 형식이 void 있고 메서드에 블록 본문 return 이 있는 경우 블록의 문(§13.10.5)은 식을 지정하지 않습니다. void 메서드의 블록 실행이 정상적으로 완료되면(즉, 컨트롤이 메서드 본문의 끝에서 흐른 경우) 해당 메서드는 호출자에게만 반환됩니다.

메서드의 유효 반환 형식이 void 메서드에 식 본문이 있는 경우 식 E은 statement_expression 되며 본문은 폼 { E; }의 블록 본문과 정확히 동일합니다.

값별 반환 메서드(§15.6.1)의 경우 해당 메서드 본문의 각 반환 문은 유효 반환 형식으로 암시적으로 변환할 수 있는 식을 지정해야 합니다.

return-by-ref 메서드(§15.6.1)의 경우, 해당 메서드 본문의 각 반환 문은 유효 반환 형식의 표현식을 명시해야 하며, 그 표현식은 호출자 컨텍스트에 대한 ref-safe-context를 가져야 합니다 (§9.7.2).

값별 반환 및 return-by-ref 메서드의 경우 메서드 본문의 엔드포인트에 연결할 수 없습니다. 즉, 컨트롤이 메서드 본문의 끝에서 흐를 수 없습니다.

예제: 다음 코드에서

class A
{
    public int F() {} // Error, return value required

    public int G()
    {
        return 1;
    }

    public int H(bool b)
    {
        if (b)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public int I(bool b) => b ? 1 : 0;
}

값 반환 F 메서드는 컨트롤이 메서드 본문의 끝에서 흐를 수 있으므로 컴파일 시간 오류가 발생합니다. G 가능한 모든 실행 경로가 반환 값을 지정하는 return 문으로 끝나기 때문에 메서드와 H 메서드는 정확합니다. 메서드는 I 본문이 단일 반환 문이 있는 블록과 동일하기 때문에 올바르습니다.

끝 예제

15.7 속성

15.7.1 일반

속성은 개체 또는 클래스의 특성에 대한 액세스를 제공하는 멤버입니다. 속성의 예로는 문자열의 길이, 글꼴 크기, 창 캡션 및 고객 이름이 있습니다. 속성은 필드의 자연스러운 확장입니다. 둘 다 연결된 형식의 명명된 멤버이며 필드 및 속성에 액세스하기 위한 구문은 동일합니다. 그러나 필드와 달리 속성은 스토리지 위치를 명시하지 않습니다. 대신, 속성에는 해당 값을 읽거나 쓸 때 실행될 문을 지정하는 접근자가 있습니다. 따라서 속성은 개체 또는 클래스 특성의 읽기 및 쓰기와 작업을 연결하기 위한 메커니즘을 제공합니다. 또한 이러한 특성을 계산할 수 있습니다.

속성은 property_declaration사용하여 선언됩니다.

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

프로퍼티 선언은 속성 집합(§22) 및 허용된 종류의 선언된 접근성(§15.3.6), , new, , static, , virtual, (§15.6.6), , override (§15.6.8)을 포함할 수 있습니다. 또한 struct_declaration에 직접 포함된 property_declarationreadonly 한정자(§16.4.11)를 포함할 수 있습니다.

  • 첫 번째는 참조 값이 아닌 속성을 선언합니다. 해당 값의 형식은 형식 입니다. 이러한 종류의 속성은 읽기 및/또는 쓰기 가능할 수 있습니다.
  • 두 번째는 참조값 속성을 선언합니다. 그 값은 variable_reference(§9.5)이며, 형식 readonly의 변수에 속할 수 있습니다. 이러한 종류의 속성은 읽기 전용입니다.

property_declaration에는 속성 집합(§22) 및 허용된 종류의 선언된 접근성(§15.3.6), new(§15.3.5), static(§15.7.2), virtual(§15.6.4, §15.7.6), override(§15.6.5, §15.7.6), sealed(§15.6.6), abstract(§15.6.7, §15.7.6), 그리고 extern 한정자(§15.6.8)가 포함될 수 있다.

속성 선언에는 유효한 한정자 조합과 관련하여 메서드 선언(§15.6)과 동일한 규칙이 적용됩니다.

member_name(§15.6.1)는 속성의 이름을 지정합니다. 속성이 명시적 인터페이스 멤버 구현 이 아니면 member_name 단순히 식별자입니다. 명시적 인터페이스 멤버 구현(§18.6.2)의 경우, member_nameinterface_type 다음에 "."와 식별자로 구성됩니다.

속성의 유형적어도 속성 자체(§7.5.5)만큼 액세스할 수 있어야 합니다.

property_body 문 본문 또는 식 본문으로 구성됩니다. 문 본문에서 접근자 선언은 반드시 "" 및 "" 토큰으로 둘러싸여야 하며, 이 선언은 속성(§15.7.3)의 접근자를 나타냅니다. 접근자가 속성 읽기 및 쓰기와 관련된 실행 문을 지정합니다.

property_body에서 => 후에 표현식E 및 세미콜론으로 구성된 식 본문은 { get { return E; } } 문 본문과 정확히 동일하므로, 이를 get 접근자의 결과가 단일 표현식으로 주어지는 읽기 전용 속성을 지정하는 데에만 사용할 수 있습니다.

자동으로 구현된 속성(§15.7.4)에 대해서만 property_initializer 제공될 수 있으며 이러한 속성의 기본 필드가 식에서 지정한 값으로 초기화됩니다.

ref_property_body 문 본문 또는 식 본문으로 구성 될 수 있습니다. 문장 본문에서 get_accessor_declaration은 속성의 get 접근자(§15.7.3)를 선언합니다. 접근자가 속성 읽기와 관련된 실행 문을 지정합니다.

ref_property_body=>, ref 다음에 variable_referenceV와 세미콜론으로 구성된 식 본문은 문 본문 { get { return ref V; } }와 정확히 동일합니다.

참고: 속성에 액세스하기 위한 구문이 필드의 구문과 동일하더라도 속성은 변수로 분류되지 않습니다. 따라서 속성이 ref-valued로 변수 참조(in)를 반환하지 않으면 속성을 out, ref, 또는 인수로 전달할 수 없습니다. 끝 메모

속성 선언에 한정자가 extern 포함된 경우 속성은 외부 속성이라고 합니다. 외부 속성 선언은 실제 구현을 제공하지 않으므로 accessor_declarations의 각 accessor_body는 세미콜론이어야 합니다.

15.7.2 정적 및 인스턴스 속성

속성 선언에 한정자가 static 포함된 경우 속성은 정적 속성이라고 합니다. 한정자가 없 static 으면 속성은 인스턴스 속성이라고 합니다.

정적 속성은 특정 인스턴스와 연결되지 않으며, 정적 속성의 접근자에서 this를 참조하는 것은 컴파일 시간 오류입니다.

인스턴스 속성은 클래스의 지정된 인스턴스와 연결되며, 해당 속성의 접근자에서 해당 인스턴스를 (this)로 참조하여 (§12.8.14) 액세스할 수 있습니다.

정적 멤버와 인스턴스 멤버의 차이점은 §15.3.8에서 자세히 설명합니다.

15.7.3 접근자

참고: 이 절은 속성(§15.7) 및 인덱서(§15.9)에 모두 적용됩니다. 이 절은 속성 측면에서 작성되며, 인덱서를 읽을 때 속성/속성을 인덱서/인덱서로 대체하고 §15.9.2에 제시된 속성과 인덱서 간의 차이점 목록을 참조합니다. 끝 메모

속성의 accessor_declarations 해당 속성 작성 및/또는 읽기와 관련된 실행 문을 지정합니다.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    | 'readonly'        // direct struct members only
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

accessor_declarationsget_accessor_declaration, set_accessor_declaration, 또는 둘 다로 구성됩니다. 각 접근자 선언은 선택적 특성, 선택적 accessor_modifier, 토큰 get 또는 set로 구성된 accessor_body로 구성됩니다.

ref_get_accessor_declaration은 ref-값 속성에 대한 선택적 특성, 선택적 accessor_modifier, 토큰 get 그리고 ref_accessor_body로 구성됩니다.

accessor_modifier사용은 다음과 같은 제한 사항이 적용됩니다.

  • accessor_modifier 인터페이스 또는 명시적 인터페이스 멤버 구현에서 사용할 수 없습니다.
  • accessor_modifierreadonlystruct_declaration에 직접 포함된 property_declaration 또는 indexer_declaration에서만 허용됩니다 (§16.4.11, §16.4.13).
  • 속성 또는 인덱서에 override 한정자가 없는 경우, get 및 set 접근자가 모두 있을 때만 accessor_modifier를 사용할 수 있으며, 그 접근자 중 하나에만 허용됩니다.
  • 한정자를 포함하는 override 속성 또는 인덱서의 경우, 접근자는 재정의되는 접근자의 accessor_modifier와 일치해야 합니다.
  • accessor_modifier 속성 또는 인덱서 자체의 선언된 접근성보다 엄격하게 더 제한적인 접근성을 선언해야 합니다. 정확하게 말하면 다음을 수행합니다.
    • 속성 또는 인덱서에 선언된 접근성이 public인 경우, accessor_modifier에 의해 접근성이 private protected, protected internal, internal, protected 또는 private 중 하나로 선언될 수 있습니다.
    • 속성 또는 인덱서에 선언된 접근성이 protected internal인 경우, accessor_modifier에 의해 접근성이 private protected, protected private, internal, protected 또는 private 중 하나로 선언될 수 있습니다.
    • 속성 또는 인덱서의 접근성이 internal 또는 protected으로 선언된 경우, accessor_modifier로 선언된 접근성은 private protected 또는 private이어야 합니다.
    • 속성 또는 인덱서에 선언된 접근성private protected이 있는 경우 accessor_modifier 선언된 접근성은 다음과 입니다private.
    • 속성 또는 인덱서에 선언된 접근성이 private있는 경우 accessor_modifier 사용할 수 없습니다.

참조 값이 아닌 abstractextern 속성에 대해, 지정된 각 접근자의 accessor_body는 단순히 세미콜론입니다. 인덱서가 아닌 비 추상 속성은 세미콜론으로 지정된 모든 접근자에 대한 accessor_body 가질 수도 있습니다. 이 경우 자동으로 구현된 속성(§15.7.4)입니다. 자동으로 구현된 속성에는 최소한 get 접근자가 있어야 합니다. 다른 비추상적이고 비외부 속성의 접근자에 대해, accessor_body는 다음 중 하나입니다.

  • 해당 접근자가 호출될 때 실행할 문을 지정하는 블록입니다.
  • 식 본문은 => 다음에 과 세미콜론으로 구성되며, 해당 접근자가 호출될 때 실행할 단일 식을 의미합니다.

참조 값 속성 abstractextern의 경우, ref_accessor_body는 단순히 세미콜론입니다. 다른 비추상적이고 extern이 아닌 속성의 접근자의 ref_accessor_body가 다음 중 하나가 됩니다.

  • get 접근자가 호출될 때 실행할 문을 지정하는 블록입니다.
  • ko-KR: => 뒤에 ref, variable_reference 및 세미콜론으로 구성된 식 본문입니다. 변수 참조는 get 접근자가 호출될 때 평가됩니다.

참조 값 형식이 아닌 속성의 get 접근자는 속성 유형의 반환 값을 갖는 매개변수 없는 메서드에 해당합니다. 할당의 대상으로 제외하면 이러한 속성이 식에서 참조되는 경우 get 접근자가 호출되어 속성 값(§12.2.2)을 계산합니다.

참조 값이 아닌 속성에 대한 get 접근자의 본문은 §15.6.11설명된 값 반환 메서드에 대한 규칙을 준수해야 합니다. 특히 get 접근자의 본문에 있는 모든 return 문은 속성 형식으로 암시적으로 변환할 수 있는 식을 지정해야 합니다. 또한 get 접근자의 엔드포인트는 접근할 수 없어야 합니다.

ref-valued 속성의 get 접근자는 반환 값이 속성 형식의 변수_참조인 매개변수 없는 메서드에 해당합니다. 이러한 속성이 식에서 참조되면 get 접근자가 호출되어 속성의 variable_reference 값을 계산합니다. 그 변수 참조는 다른 변수와 마찬가지로, 읽거나 또는 변수_참조가 아닌 경우에는 컨텍스트에서 요구하는 대로 참조된 변수를 쓸 때 사용됩니다.

예제: 다음 예제에서는 참조 값 속성을 할당의 대상으로 보여 줍니다.

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

끝 예제

ref-valued 속성에 대한 get 접근자의 본문은 §15.6.11에 설명된 ref-valued 메서드에 대한 규칙을 준수해야 합니다.

set 접근자는 속성 형식의 단일 값 매개 변수를 갖는 메서드와 void 반환 형식에 해당합니다. set 접근자의 암시적 매개 변수는 항상 value이라는 이름이 지정됩니다. 속성이 할당 대상(§12.21) 또는 피연산자++(–-§12.8.16, §12.9.6)로 참조되는 경우 set 접근자는 새 값(§12.21.2)을 제공하는 인수를 사용하여 호출됩니다. 집합 접근자의 본문은 void메서드에 대해 §15.6.11에 설명된 규칙을 준수해야 합니다. 특히 set 접근자 본문의 return 문에서는 식을 지정할 수 없습니다. 설정 접근자에는 value라는 암시적인 매개 변수가 있기 때문에, 설정 접근자 내에서 이 이름을 사용하여 지역 변수나 상수를 선언하는 것은 컴파일 시 오류입니다.

get 및 set 접근자의 존재 여부 또는 부재에 따라 속성은 다음과 같이 분류됩니다.

  • get 접근자와 set 접근자를 모두 포함하는 속성은 읽기/쓰기 속성이라고 합니다.
  • get 접근자만 있는 속성은 읽기 전용 속성이라고 합니다. 읽기 전용 속성이 할당의 대상이 되는 것은 컴파일 시간 오류입니다.
  • set 접근자만 있는 속성은 쓰기 전용 속성이라고 합니다. 할당의 대상으로 제외하면 식에서 쓰기 전용 속성을 참조하는 것은 컴파일 시간 오류입니다.

참고: 전위 ++ 및 후위 -- 연산자와 복합 할당 연산자는 새 값을 쓰기 전에 피연산자의 이전 값을 읽으므로, 쓰기 전용 속성에 적용할 수 없습니다. 끝 메모

예제: 다음 코드에서

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => caption;
        set
        {
            if (caption != value)
            {
                caption = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

컨트롤이 Button public Caption 속성을 선언합니다. Caption 속성의 get 접근자는 개인 string 필드에 저장된 caption을 반환합니다. set 접근자가 새 값이 현재 값과 다른지 확인하고, 있는 경우 새 값을 저장하고 컨트롤을 다시 칠합니다. 속성은 종종 위에 표시된 패턴을 따릅니다. get 접근자는 필드에 저장된 private 값을 반환하고 집합 접근자는 해당 private 필드를 수정한 다음 개체의 상태를 완전히 업데이트하는 데 필요한 추가 작업을 수행합니다. 위의 Button 클래스를 고려하여, Caption 속성 사용의 예는 다음과 같습니다.

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

여기서 set 접근자가 속성에 값을 할당하여 호출되고 식에서 속성을 참조하여 get 접근자가 호출됩니다.

끝 예제

속성의 get 및 set 접근자는 고유 멤버가 아니며 속성의 접근자를 별도로 선언할 수 없습니다.

: 예제

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

는 단일 읽기-쓰기 속성을 선언하지 않습니다. 대신 이름이 같은 두 개의 속성을 선언합니다. 하나는 읽기 전용이고 다른 하나는 쓰기 전용입니다. 동일한 클래스에 선언된 두 멤버의 이름은 같을 수 없으므로 컴파일 시간 오류가 발생하는 예제입니다.

끝 예제

파생 클래스가 상속된 속성과 동일한 이름으로 속성을 선언하는 경우 파생 속성은 읽기 및 쓰기와 관련하여 상속된 속성을 숨깁니다.

예제: 다음 코드에서

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

P 속성은 읽기 및 쓰기에 있어 BP 속성을 A에서 가립니다. 따라서 진술에서

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

b.P의 읽기 전용 P 속성이 B의 쓰기 전용 P 속성을 숨기므로 A에 대한 할당은 컴파일 시간 오류를 발생시킵니다. 주의할 점은, 캐스트를 사용하여 숨겨진 P 속성에 액세스할 수 있다는 것입니다.

끝 예제

공용 필드와 달리 속성은 개체의 내부 상태와 공용 인터페이스를 구분합니다.

예제: 구조체를 Point 사용하여 위치를 나타내는 다음 코드를 고려합니다.

class Label
{
    private int x, y;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.x = x;
        this.y = y;
        this.caption = caption;
    }

    public int X => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

여기서 클래스는 Labelint 개의 필드를 xy사용하고 위치를 저장합니다. 위치는 XY 속성으로서, 형식 LocationPoint 속성으로 공개적으로 노출됩니다. 이후 버전의 Label경우 내부적으로 Point 위치를 저장하는 것이 더 편리해지면 클래스의 공용 인터페이스에 영향을 주지 않고 변경할 수 있습니다.

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

xypublic readonly 대신 필드였다면, Label 클래스를 그렇게 변경하는 것은 불가능했을 것입니다.

끝 예제

참고: 속성을 통해 상태를 노출하는 것이 필드를 직접 노출하는 것보다 덜 효율적인 것은 아닙니다. 특히 속성이 가상이 아니고 적은 양의 코드만 포함된 경우 실행 환경에서 접근자에 대한 호출을 접근자의 실제 코드로 바꿀 수 있습니다. 이 프로세스를 인라인 처리라고 하며 필드 액세스만큼 효율적으로 속성에 액세스할 수 있지만 속성의 유연성이 향상됩니다. 끝 메모

: get 접근자를 호출하는 것은 개념적으로 필드 값을 읽는 것과 동일하기 때문에 get 접근자가 관찰 가능한 부작용을 가지는 것은 잘못된 프로그래밍 스타일로 간주됩니다. 예제에서

class Counter
{
    private int next;

    public int Next => next++;
}

속성 값은 Next 이전에 속성에 액세스한 횟수에 따라 달라집니다. 따라서 속성에 액세스하면 관찰 가능한 부작용이 발생하며 대신 메서드로 속성을 구현해야 합니다.

get 접근자에 대한 "부작용 없음" 규칙이 필드에 저장된 값을 반환하기 위해 get 접근자를 항상 작성해야 한다는 의미는 아닙니다. 실제로 get 접근자는 여러 필드에 액세스하거나 메서드를 호출하여 속성 값을 계산하는 경우가 많습니다. 그러나 올바르게 설계된 get 접근자는 개체의 상태에 눈에 띄는 변화를 일으키는 작업을 수행하지 않습니다.

끝 예제

속성을 사용하여 리소스가 처음 참조될 때까지 리소스 초기화를 지연할 수 있습니다.

예제:

public class Console
{
    private static TextReader reader;
    private static TextWriter writer;
    private static TextWriter error;

    public static TextReader In
    {
        get
        {
            if (reader == null)
            {
                reader = new StreamReader(Console.OpenStandardInput());
            }
            return reader;
        }
    }

    public static TextWriter Out
    {
        get
        {
            if (writer == null)
            {
                writer = new StreamWriter(Console.OpenStandardOutput());
            }
            return writer;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (error == null)
            {
                error = new StreamWriter(Console.OpenStandardError());
            }
            return error;
        }
    }
...
}

클래스에는 표준 입력, 출력 및 오류 디바이스를 나타내는 세 가지 속성인 Console, In, OutError이 포함됩니다. 이러한 멤버를 속성으로 노출하면 클래스가 Console 실제로 사용될 때까지 초기화를 지연할 수 있습니다. 예를 들어, 속성을 처음 참조할 때 Out 와 같이

Console.Out.WriteLine("hello, world");

출력 디바이스의 기본 TextWriter 이 만들어집니다. 그러나 애플리케이션이 InError 속성을 참조하지 않으면, 해당 장치에 대해 개체가 만들어지지 않습니다.

끝 예제

15.7.4 자동으로 구현된 속성

자동으로 구현된 속성(또는 자동 속성)은 세미콜론만 있는 accessor_body를 가진, 비추상적이면서 비extern이며 ref-값이 아닌 속성입니다. 자동 속성에는 get 접근자가 있어야 하며 필요에 따라 set 접근자가 있을 수 있습니다.

속성이 자동으로 구현된 속성으로 지정되면 숨겨진 지원 필드를 속성에 자동으로 사용할 수 있으며 접근자는 해당 지원 필드에서 읽고 쓰도록 구현됩니다. 숨겨진 지원 필드는 액세스할 수 없으며, 포함된 형식 내에서도 자동으로 구현된 속성 접근자를 통해서만 읽고 쓸 수 있습니다. auto 속성에 set 접근자가 없으면 지원 필드는 readonly로 간주됩니다 (§15.5.3). 필드와 readonly 마찬가지로, 읽기 전용 자동 속성도 포함하는 클래스의 생성자 본문에 할당될 수 있습니다. 이러한 할당은 속성의 읽기 전용 백업 필드에 직접 할당됩니다.

자동 속성은 선택적으로 속성_초기화자를 가질 수 있으며, 이것은 변수_초기화자로서 지원 필드에 직접 적용됩니다 (§17.7).

예제:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

는 다음 선언과 동일합니다.

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

끝 예제

: 다음에서

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

는 다음 선언과 동일합니다.

public class ReadOnlyPoint
{
    private readonly int __x;
    private readonly int __y;
    public int X { get { return __x; } }
    public int Y { get { return __y; } }

    public ReadOnlyPoint(int x, int y)
    {
        __x = x;
        __y = y;
    }
}

읽기 전용 필드에 대한 할당은 생성자 내에서 발생하므로 유효합니다.

끝 예제

지원 필드는 숨겨져 있지만, 자동 구현된 속성의 property_declaration (§15.7.1)을 통해 필드 대상 특성을 그 필드에 직접 적용할 수 있습니다.

예제: 다음 코드

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

그러면 다음과 같이 코드가 작성된 것처럼 필드 대상 특성 NonSerialized 이 컴파일러에서 생성된 지원 필드에 적용됩니다.

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

끝 예제

15.7.5 접근성

접근자에 accessor_modifier 있는 경우 접근자의 접근성 도메인(§7.5.3)은 accessor_modifier 선언된 접근성을 사용하여 결정됩니다. 접근자에 accessor_modifier 없는 경우 접근자의 접근성 도메인은 속성 또는 인덱서의 선언된 접근성에서 결정됩니다.

accessor_modifier의 존재는 멤버 조회(§12.5)나 오버로드 결정(§12.6.4)에 절대 영향을 미치지 않습니다. 속성 또는 인덱서의 한정자는 액세스 컨텍스트에 관계없이 항상 바인딩되는 속성 또는 인덱서를 결정합니다.

특정 비-ref-valued 속성 또는 비-ref-valued 인덱서가 선택되면 관련 접근자의 접근성 도메인을 사용하여 해당 사용이 유효한지 확인합니다.

  • 사용량이 값(§12.2.2)인 경우 get 접근자가 존재하여 액세스할 수 있어야 합니다.
  • 사용량이 단순 할당(§12.21.2)의 대상으로 지정된 경우 set 접근자가 존재하여 액세스할 수 있어야 합니다.
  • 사용이 복합 대입(§12.21.4)의 목표이거나, ++ 또는 -- 연산자(§12.8.16, §12.9.6)의 목표인 경우, get 접근자와 set 접근자가 모두 존재하고 접근 가능해야 합니다.

: 다음 예제에서는 A.Text 속성이, set 접근자만 호출되는 컨텍스트에서도 B.Text 속성에 의해 숨겨집니다. 반면, 클래스에서 속성 B.CountM액세스할 수 없으므로 액세스 가능한 속성 A.Count 이 대신 사용됩니다.

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => count;
        set => count = value;
    }
}

class M
{
    static void Main()
    {
        B b = new B();
        b.Count = 12;       // Calls A.Count set accessor
        int i = b.Count;    // Calls A.Count get accessor
        b.Text = "howdy";   // Error, B.Text set accessor not accessible
        string s = b.Text;  // Calls B.Text get accessor
    }
}

끝 예제

특정 ref-valued 속성 또는 ref-valued 인덱서가 선택되면 사용법이 값인지, 단순 할당의 대상인지, 복합 할당의 대상인지에 관계없이 관련 get 접근자의 접근성 도메인을 사용하여 해당 사용이 유효한지 확인합니다.

인터페이스를 구현하는 데 사용되는 접근자에는 accessor_modifier가 없어야 합니다. 인터페이스를 구현하는 데 한 접근자만 사용하는 경우 다른 접근자는 accessor_modifier 사용하여 선언될 수 있습니다.

예제:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

끝 예제

15.7.6 가상, 봉인, 재정의 및 추상 접근자

참고: 이 절은 속성(§15.7) 및 인덱서(§15.9)에 모두 적용됩니다. 이 절은 속성 측면에서 작성되며, 인덱서를 읽을 때 속성/속성을 인덱서/인덱서로 대체하고 §15.9.2에 제시된 속성과 인덱서 간의 차이점 목록을 참조합니다. 끝 메모

가상 속성 선언은 속성의 접근자가 가상임을 지정합니다. virtual 수정자는 속성의 모든 비공개 접근자에 적용된다. 가상 속성 private의 접근자에 accessor_modifier 있는 경우 프라이빗 접근자가 암시적으로 가상이 아닙니다.

추상 속성 선언은 속성의 접근자가 가상이지만 접근자의 실제 구현을 제공하지 않음을 지정합니다. 대신 속성을 재정의하여 접근자에 대한 고유한 구현을 제공하려면 비 추상 파생 클래스가 필요합니다. 추상 속성 선언에 대한 접근자가 실제 구현을 제공하지 않으므로 해당 accessor_body 단순히 세미콜론으로 구성됩니다. 추상 속성에는 접근자가 private 없어야 합니다.

abstractoverride 한정자를 포함하는 속성 선언은 해당 속성이 추상적이며 기본 속성을 재정의함을 지정합니다. 이러한 속성의 접근자도 추상적입니다.

추상 속성 선언은 추상 클래스(§15.2.2.2)에서만 허용됩니다. 상속된 가상 속성의 접근자는 지시문을 포함한 속성 선언을 사용하여 파생 클래스에서 override 재정의할 수 있습니다. 이것을 재정의 속성 선언이라고 합니다. 재정의된 속성 선언은 새로운 속성을 추가하지 않습니다. 대신 기존 가상 속성의 접근자의 구현을 특수화합니다.

재정의 선언과 재정의된 기본 속성은 동일한 수준의 접근성을 선언해야 합니다. 즉, 재정의 선언은 기초 속성의 접근성을 변경해서는 안 됩니다. 그러나 재정의된 기본 속성이 내부적으로 보호되고 재정의 선언을 포함하는 어셈블리와 다른 어셈블리에 선언된 경우 재정의 선언의 선언된 접근성이 보호됩니다. 상속된 속성에 단일 접근자만 있는 경우(상속된 속성이 읽기 전용이거나 쓰기 전용인 경우) 오버라이딩된 속성은 해당 접근자만 포함해야 합니다. 상속된 속성에 접근자가 두 개 있는 경우(즉, 상속된 속성이 읽기와 쓰기가 가능한 경우), 재정의 속성은 단일 접근자 또는 두 접근자를 모두 포함할 수 있습니다. 재정의 형식과 상속된 속성 간에 동일성 변환이 있어야 합니다.

재정의 속성 선언에는 sealed 수정자가 포함될 수 있습니다. 이 한정자를 사용하면 파생 클래스가 속성을 추가로 재정의할 수 없습니다. 봉인된 속성의 접근자 또한 봉인됩니다.

선언 및 호출 구문의 차이를 제외하고 가상, 봉인, 재정의 및 추상 접근자가 가상, 봉인, 재정의 및 추상 메서드와 똑같이 동작합니다. 특히 §15.6.4, §15.6.5, §15.6.6 및 §15.6.7에 설명된 규칙은 접근자가 해당 양식의 메서드인 것처럼 적용됩니다.

  • get 접근자는 속성 형식의 반환 값과 포함하는 속성과 동일한 한정자를 가진 매개 변수가 없는 메서드에 해당합니다.
  • set 접근자는 속성 형식의 단일 값 매개 변수, void 반환 형식 및 포함하는 속성과 동일한 한정자가 있는 메서드에 해당합니다.

예제: 다음 코드에서

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X 는 가상 읽기 전용 속성이고, Y 가상 읽기-쓰기 속성이며 Z , 추상 읽기-쓰기 속성입니다. Z 추상이므로 포함하는 클래스 A도 추상으로 선언되어야 합니다.

다음은 A에서 파생된 클래스입니다.

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

여기서는 X, Y, Z가 속성 선언을 재정의합니다. 각 속성 선언은 해당 상속된 속성의 접근성 한정자, 형식 및 이름과 정확히 일치합니다. X의 get 접근자와 Y의 set 접근자는 기본 키워드를 사용하여 상속된 접근자에 액세스합니다. Z 선언은 두 추상 접근자를 재정의합니다. 따라서 abstract에는 남은 B 함수 멤버가 없으며 B는 비추상 클래스가 될 수 있습니다.

끝 예제

속성이 재정의로 선언될 때, 모든 재정의된 접근자는 재정의하는 코드에서 액세스할 수 있어야 합니다. 또한 속성 또는 인덱서 자체와 접근자의 선언된 접근 제한 설정은 재정의된 멤버 및 접근자의 접근 제한 설정과 일치해야 합니다.

예제:

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

끝 예제

15.8 이벤트

15.8.1 일반

이벤트는 개체 또는 클래스가 알림을 제공할 수 있도록 하는 멤버입니다. 클라이언트는 이벤트 처리기를 제공하여 이벤트에 대한 실행 코드를 연결할 수 있습니다.

이벤트는 event_declaration사용하여 선언됩니다.

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

event_declaration에는 특성 집합(§22)과 선언된 접근성의 허용된 종류(§15.3.6), new (§15.3.5), static (§15.6.3, §15.8.4), virtual (§15.6.4, §15.8.5), override (§15.6.5, §15.8.5), sealed (§15.6.6), abstract (§15.6.7, §15.8.5), 및 extern (§15.6.8) 한정자가 포함될 수 있습니다. 또한 struct_declaration에 직접 포함된 event_declarationreadonly 한정자(§16.4.12)를 포함할 수 있습니다.

이벤트 선언에는 유효한 한정자 조합과 관련하여 메서드 선언(§15.6)과 동일한 규칙이 적용됩니다.

이벤트 선언의 유형delegate_type(§8.2.8)이어야 하며, 해당 delegate_type 이벤트 자체(§7.5.5)만큼 액세스할 수 있어야 합니다.

이벤트 선언에는 event_accessor_declaration포함될 수 있습니다. 그러나 비-extern, 비추상 이벤트의 경우 컴파일러는 자동으로 제공해야 합니다(§15.8.2); extern 이벤트의 경우 엑세서는 외부에서 제공됩니다.

event_accessor_declaration을 생략한 이벤트 선언은 변수 선언자 각각에 대해 하나 이상의 이벤트를 정의합니다. 특성 및 한정자는 이러한 event_declaration 선언된 모든 멤버에 적용됩니다.

event_declarationabstractevent_accessor_declaration이 모두 포함될 경우 컴파일 시간 오류가 발생합니다.

이벤트 선언에 한 extern 정자가 포함된 경우 이벤트는 외부 이벤트라고 합니다. 외부 이벤트 선언은 실제 구현을 제공하지 않기 때문에 extern 한정자와 event_accessor_declaration를 모두 포함하는 것은 오류로 간주됩니다.

또는 abstract 한정자가 있는 이벤트 선언에서 externalvariable_initializer를 포함하는 것은 컴파일 시간 오류입니다.

이벤트는 +=-= 연산자의 왼쪽 피연산자로 사용할 수 있습니다. 이러한 연산자는 이벤트 처리기를 이벤트 처리기에 연결하거나 이벤트에서 이벤트 처리기를 제거하는 데 각각 사용되며, 이벤트의 액세스 한정자는 이러한 작업이 허용되는 컨텍스트를 제어합니다.

해당 이벤트가 선언된 형식 외부의 코드에 의해 이벤트에 허용되는 유일한 작업은 다음과 +=같습니다-=. 따라서 이러한 코드는 이벤트에 대한 처리기를 추가 및 제거할 수 있지만 이벤트 처리기의 기본 목록을 직접 가져오거나 수정할 수는 없습니다.

연산의 형태가 x += y 또는 x –= y인 경우에, x가 이벤트라면 연산의 결과는 형식 void을 갖습니다(§12.21.5). 이는 이벤트가 아닌 형식에 정의된 다른 xx 연산자와 달리, 할당 후 +=의 값을 갖는 -= 형식을 갖는 것에 반대되는 경우입니다. 이렇게 하면 외부 코드가 이벤트의 기본 대리자를 간접적으로 검사하지 못하게 됩니다.

예제: 다음 예제에서는 이벤트 처리기가 클래스의 Button 인스턴스에 연결되는 방법을 보여 줍니다.

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

LoginDialog 여기서 인스턴스 생성자는 두 개의 Button 인스턴스를 만들고 이벤트 처리기를 이벤트에 연결합니다Click.

끝 예제

15.8.2 필드와 유사한 이벤트

이벤트 선언을 포함하는 클래스 또는 구조체의 프로그램 텍스트 내에서 특정 이벤트를 필드처럼 사용할 수 있습니다. 이러한 방식으로 사용하려면 이벤트는 추상적이거나 외설적이지 않으며 event_accessor_declaration명시적으로 포함하지 않아야 합니다. 이러한 이벤트는 필드를 허용하는 모든 컨텍스트에서 사용할 수 있습니다. 필드에는 이벤트에 추가된 이벤트 처리기 목록을 참조하는 대리자(§20)가 포함됩니다. 이벤트 처리기가 추가되지 않은 경우 필드에는 null.

예제: 다음 코드에서

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

Click 는 클래스 내 Button 의 필드로 사용됩니다. 예제에서 알 수 있듯이 대리자 호출 식에서 필드를 검사, 수정 및 사용할 수 있습니다. OnClick 클래스의 Button 메서드는 Click 이벤트를 "발생"시킵니다. 이벤트 발생 개념은 이벤트가 나타내는 대리자를 호출하는 것과 정확히 동일하므로 이벤트 발생을 위한 특수한 언어 구문은 없습니다. 대리자 호출 앞에는 대리자가 null이 아닌지 확인하고 스레드 안전을 보장하기 위해 로컬 복사본에서 검사가 수행되는지 확인합니다.

클래스 선언 Button 외부에서는 Click 멤버를 +=–= 연산자의 왼쪽에서만 사용할 수 있습니다.

b.Click += new EventHandler(...);

Click 이벤트의 호출 목록에 델리게이트를 추가하는 것.

Click –= new EventHandler(...);

Click 이벤트의 호출 목록에서 대리자를 제거합니다.

끝 예제

필드와 유사한 이벤트를 컴파일할 때 컴파일러는 대리자를 보관할 스토리지를 자동으로 만들고 대리자 필드에 이벤트 처리기를 추가하거나 제거하는 이벤트에 대한 접근자를 만들어야 합니다. 추가 및 제거 작업은 스레드로부터 안전하며, 인스턴스 이벤트의 경우 포함 객체에 대한 잠금(§13.13)을 유지한 채로 수행될 수 있고, 정적 이벤트의 경우에는 객체(System.Type)에 대한 잠금 상태에서도 수행될 수 있습니다. 하지만, 이를 반드시 해야 하는 것은 아닙니다.

참고: 따라서 폼의 인스턴스 이벤트 선언은 다음과 같습니다.

class X
{
    public event D Ev;
}

다음과 동등한 것으로 컴파일되어야 합니다.

class X
{
    private D __Ev; // field to hold the delegate

    public event D Ev
    {
        add
        {
            /* Add the delegate in a thread safe way */
        }
        remove
        {
            /* Remove the delegate in a thread safe way */
        }
    }
}

클래스 X 내에서, Ev+= 연산자의 왼쪽에 있는 –=에 대한 참조로 인해 add 및 remove 접근자가 호출됩니다. 다른 모든 참조 Ev 는 숨겨진 필드를 __Ev 대신 참조하도록 컴파일됩니다(§12.8.7). 이름 "__Ev"은 임의입니다. 숨겨진 필드에는 이름이 있거나 이름이 전혀 없을 수 있습니다.

끝 메모

15.8.3 이벤트 접근자

참고: 이벤트 선언에서는 일반적으로 event_accessor_declaration을 생략합니다. 위의 Button 예제가 그런 경우입니다. 예를 들어 이벤트당 하나의 필드의 스토리지 비용이 허용되지 않는 경우 포함할 수 있습니다. 이러한 경우 클래스는 event_accessor_declaration포함할 수 있으며 이벤트 처리기 목록을 저장하기 위한 프라이빗 메커니즘을 사용할 수 있습니다. 끝 메모

이벤트의 event_accessor_declarations 이벤트 처리기 추가 및 제거와 관련된 실행 문을 지정합니다.

접근자 선언은 add_accessor_declarationremove_accessor_declaration로 구성됩니다. 각 접근자 선언은 토큰 추가 또는 제거와 블록으로 구성됩니다. add_accessor_declaration에 연결된 블록은 이벤트 처리기가 추가될 때 실행할 문을 지정하고, remove_accessor_declaration에 연결된 블록은 이벤트 처리기가 제거될 때 실행할 문을 지정합니다.

add_accessor_declarationremove_accessor_declaration 이벤트 형식의 단일 값 매개 변수 및 반환 형식이 있는 메서드에 void 해당합니다. 이벤트 접근자의 암시적 매개 변수 이름은 value.입니다. 이벤트 할당에서 이벤트를 사용하는 경우 적절한 이벤트 접근자가 사용됩니다. 특히 대입 연산 += 자가 있는 경우 add 접근자가 사용되고 할당 연산 –= 자가 있으면 제거 접근자가 사용됩니다. 두 경우 모두 할당 연산자의 오른쪽 피연산자가 이벤트 접근자에 대한 인수로 사용됩니다. add_accessor_declaration 블록이나 remove_accessor_declaration 블록은 §15.6.9에 설명된 void 메서드에 대한 규칙을 준수해야 합니다. 특히 return 이러한 블록의 문에서는 식을 지정하면 안 됩니다.

이벤트 접근자에는 암시적으로 value라는 이름의 매개 변수가 있으므로, 이벤트 접근자 내에서 해당 이름으로 지역 변수 또는 상수를 선언하는 것은 컴파일 시간 오류입니다.

예제: 다음 코드에서


class Control : Component
{
    // Unique keys for events
    static readonly object mouseDownEventKey = new object();
    static readonly object mouseUpEventKey = new object();

    // Return event handler associated with key
    protected Delegate GetEventHandler(object key) {...}

    // Add event handler associated with key
    protected void AddEventHandler(object key, Delegate handler) {...}

    // Remove event handler associated with key
    protected void RemoveEventHandler(object key, Delegate handler) {...}

    // MouseDown event
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler(mouseDownEventKey, value); }
        remove { RemoveEventHandler(mouseDownEventKey, value); }
    }

    // MouseUp event
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler(mouseUpEventKey, value); }
        remove { RemoveEventHandler(mouseUpEventKey, value); }
    }

    // Invoke the MouseUp event
    protected void OnMouseUp(MouseEventArgs args)
    {
        MouseEventHandler handler;
        handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
        if (handler != null)
        {
            handler(this, args);
        }
    }
}

클래스는 Control 이벤트에 대한 내부 스토리지 메커니즘을 구현합니다. 메서드는 AddEventHandler 대리자 값을 키와 연결하고, GetEventHandler 메서드는 현재 키와 연결된 대리자를 반환하고 RemoveEventHandler , 메서드는 지정된 이벤트에 대한 이벤트 처리기로 대리자를 제거합니다. 아마도 기본 스토리지 메커니즘은 null 대리자 값을 키와 연결하기 위한 비용이 없도록 설계되었으므로 처리되지 않은 이벤트는 스토리지를 사용하지 않습니다.

끝 예제

15.8.4 정적 및 인스턴스 이벤트

이벤트 선언에 한 static 정자가 포함된 경우 이벤트는 정적 이벤트라고 합니다. 한정자가 없 static 으면 이벤트를 인스턴스 이벤트라고 합니다.

정적 이벤트는 특정 인스턴스에 연결되지 않습니다. 정적 이벤트의 접근자에서 this를 참조하면 컴파일 시간 오류가 발생합니다.

인스턴스 이벤트는 클래스의 지정된 인스턴스와 연결되며, 이 인스턴스는 해당 이벤트의 접근자에서 this(§12.8.14)로 액세스할 수 있습니다.

정적 멤버와 인스턴스 멤버의 차이점은 §15.3.8에서 자세히 설명합니다.

15.8.5 가상, 봉인, 오버라이드 및 추상 접근자

가상 이벤트 선언은 해당 이벤트의 접근자가 가상임을 지정합니다. virtual 수정자는 이벤트의 두 접근자에 모두 적용됩니다.

추상 이벤트 선언은 이벤트의 접근자가 가상이지만 접근자의 실제 구현을 제공하지 않음을 지정합니다. 대신 비 추상 파생 클래스는 이벤트를 재정의하여 접근자에 대한 고유한 구현을 제공해야 합니다. 추상 이벤트 선언에 대한 접근자는 실제 구현을 제공하지 않으므로 event_accessor_declaration을 제공해서는 안 됩니다.

abstractoverride 한정자를 모두 포함하는 이벤트 선언은 해당 이벤트가 추상적이며 기본 이벤트를 재정의함을 지정합니다. 이러한 이벤트의 접근자는 추상적이라는 점에서도 그렇습니다.

추상 이벤트 선언은 추상 클래스(§15.2.2.2)에서만 허용됩니다.

상속된 가상 이벤트의 접근자는 override 한정자를 지정한 이벤트 선언을 포함함으로써 파생 클래스에서 재정의할 수 있습니다. 이는 재정의 이벤트 선언이라고 합니다. 재정의 이벤트 선언은 기존 이벤트를 새로 선언하지 않습니다. 대신 기존 가상 이벤트의 접근자의 구현을 특수화합니다.

재정의 이벤트 선언은 재정의된 이벤트와 정확히 동일한 접근성 한정자와 이름을 지정해야 하며, 재정의 형식과 재정의된 이벤트 간에 ID 변환이 있어야 하며, 추가 및 제거 접근자를 선언 내에 지정해야 합니다.

재정의 이벤트 선언에는 sealed 한정자가 포함될 수 있습니다. this 한정자를 사용하면 파생 클래스가 이벤트를 더 이상 재정의하지 못하게 합니다. 봉인된 이벤트의 접근자도 봉인됩니다.

재정의 이벤트 선언에 new 한정자를 포함하는 것은 컴파일 시간 오류입니다.

선언 및 호출 구문의 차이를 제외하고 가상, 봉인, 재정의 및 추상 접근자가 가상, 봉인, 재정의 및 추상 메서드와 똑같이 동작합니다. 특히 §15.6.4, §15.6.5, §15.6.6 및 §15.6.7에 설명된 규칙은 접근자가 해당 양식의 메서드인 것처럼 적용됩니다. 각 접근자는 이벤트 형식의 단일 값 매개 변수, void 반환 형식 및 포함하는 이벤트와 동일한 한정자가 있는 메서드에 해당합니다.

15.9 인덱서

15.9.1 일반

덱서는 배열과 동일한 방식으로 개체를 인덱싱할 수 있는 멤버입니다. 인덱서는 indexer_declaration사용하여 선언됩니다.

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | 'readonly'        // direct struct members only
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

indexer_declaration특성 집합(§22)과 허용된 종류의 선언된 접근성(§15.3.6), new (§15.3.5), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) 한정자 중 하나를 포함할 수 있습니다. 또한 struct_declaration에 직접 포함된 indexer_declarationreadonly 한정자(§16.4.12)를 포함할 수 있습니다.

  • 첫 번째는 참조 값이 아닌 인덱서를 선언합니다. 해당 값의 형식은 형식 입니다. 이러한 종류의 인덱서는 읽을 수 있거나 쓸 수 있습니다.
  • 두 번째는 참조 값을 가지는 인덱서를 선언합니다. 그 값은 variable_reference(§9.5)이며, 형식 readonly의 변수에 속할 수 있습니다. 이러한 종류의 인덱서는 읽기 전용입니다.

indexer_declaration에는 속성들(§22)의 집합 및 허용된 종류의 선언된 접근성(§15.3.6), new(§15.3.5), virtual(§15.6.4), override(§15.6.5), sealed(§15.6.6), abstract(§15.6.7) 및 extern(§15.6.8) 한정자 중 하나를 포함할 수 있습니다.

인덱서 선언에는 유효한 한정자 조합과 관련하여 메서드 선언(§15.6)과 동일한 규칙이 적용되며 static 한 가지 예외는 한정자가 인덱서 선언에서 허용되지 않는다는 것입니다.

인덱서 선언의 형식선언에 의해 도입된 인덱서의 요소 형식을 지정합니다.

참고: 인덱서는 배열 요소와 같은 컨텍스트에서 사용하도록 설계되므로 배열에 정의된 용어 요소 형식 도 인덱서와 함께 사용됩니다. 끝 메모

인덱서가 명시적 인터페이스 멤버 구현이 아니면 형식 뒤에 키워드 this가 있습니다. 명시적 인터페이스 멤버 구현의 경우 형식 뒤에 interface_type, "." 및 키워드 this가 잇습니다. 다른 멤버와 달리 인덱서에는 사용자 정의 이름이 없습니다.

parameter_list 인덱서의 매개 변수를 지정합니다. 인덱서의 매개 변수 목록은 하나 이상의 매개 변수를 지정해야 하며 , thisref 및 매개 변수 한정자가 허용되지 않는다는 점을 제외하고 메서드(out)의 매개 변수 목록에 해당합니다.

인덱서의 형식parameter_list에 참조된 각 형식은 적어도 인덱서 자체만큼(§7.5.5) 접근할 수 있어야 합니다.

indexer_body 문 본문(§15.7.1) 또는 식 본문(§15.6.1)으로 구성됩니다. 문장 본문에서 액세서 선언은 반드시 “{” 및 “}” 토큰으로 둘러싸여야 하며, 인덱서의 접근자(§15.7.3)를 선언합니다. 접근자는 인덱서 요소 읽기 및 쓰기와 관련된 실행 문을 지정합니다.

indexer_body는 “=>”와 이어지는 표현 E 및 세미콜론으로 구성된 표현 본문으로, 이는 문 본문 { get { return E; } }과 완전히 동일합니다. 따라서 이는 get 접근자의 결과가 단일 표현식에 의해 제공되는 읽기 전용 인덱서를 지정하는 데만 사용할 수 있습니다.

ref_indexer_body 문 본문 또는 식 본문으로 구성됩니다. 문 본문에서 get_accessor_declaration은 인덱서의 get 접근자(§15.7.3)를 선언합니다. 접근자는 인덱서 읽기와 관련된 실행 문을 지정합니다.

ref_indexer_body에서 =>refvariable_reference 및 세미콜론으로 구성된 식 본문은 문 본문 V과 정확히 동일합니다.

참고: 인덱서 요소에 액세스하기 위한 구문이 배열 요소의 구문과 동일하더라도 인덱서 요소는 변수로 분류되지 않습니다. 따라서 인덱서가 ref-valued이어서 참조를 반환할 경우가 아니면 인덱서 요소를 , , 또는 인수로 전달할 수 없습니다 (§9.7). 끝 메모

인덱서의 parameter_list 인덱서의 서명(§7.6)을 정의합니다. 특히 인덱서의 서명은 매개 변수의 수와 형식으로 구성됩니다. 매개 변수의 요소 형식 및 이름은 인덱서 서명의 일부가 아닙니다.

인덱서의 서명은 동일한 클래스에 선언된 다른 모든 인덱서의 서명과 다릅니다.

인덱서 선언에 한 extern 정자가 포함된 경우 인덱서는 외부 인덱서라고 합니다. 외부 인덱서 선언은 실제 구현을 제공하지 않으므로, accessor_bodyaccessor_declarations에서 모두 세미콜론이어야 합니다.

예제: 아래 예제에서는 비트 배열의 BitArray 개별 비트에 액세스하기 위한 인덱서 구현 클래스를 선언합니다.

class BitArray
{
    int[] bits;
    int length;

    public BitArray(int length)
    {
        if (length < 0)
        {
            throw new ArgumentException();
        }
        bits = new int[((length - 1) >> 5) + 1];
        this.length = length;
    }

    public int Length => length;

    public bool this[int index]
    {
        get
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            return (bits[index >> 5] & 1 << index) != 0;
        }
        set
        {
            if (index < 0 || index >= length)
            {
                throw new IndexOutOfRangeException();
            }
            if (value)
            {
                bits[index >> 5] |= 1 << index;
            }
            else
            {
                bits[index >> 5] &= ~(1 << index);
            }
        }
    }
}

클래스 BitArray의 인스턴스는 bool[] 인스턴스보다 훨씬 적은 메모리를 소모합니다(전자의 각 값은 byte 대신 1비트만 차지하기 때문입니다), 하지만 bool[]와 동일한 작업을 수행할 수 있습니다.

다음 CountPrimes 클래스는 BitArray과 고전적인 "체" 알고리즘을 사용하여 2에서 지정된 최대 값 사이의 소수 개수를 계산합니다.

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        for (int i = 2; i <= max; i++)
        {
            if (!flags[i])
            {
                for (int j = i * 2; j <= max; j += i)
                {
                    flags[j] = true;
                }
                count++;
            }
        }
        return count;
    }

    static void Main(string[] args)
    {
        int max = int.Parse(args[0]);
        int count = Count(max);
        Console.WriteLine($"Found {count} primes between 2 and {max}");
    }
}

BitArray의 요소에 접근하는 구문은 bool[]의 구문과 정확히 동일합니다.

다음 예제에서는 두 개의 매개 변수가 있는 인덱서가 있는 26×10 그리드 클래스를 보여줍니다. 첫 번째 매개 변수는 A-Z 범위의 대문자 또는 소문자여야 하며, 두 번째 매개 변수는 0-9 범위의 정수여야 합니다.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

끝 예제

15.9.2 인덱서 및 속성 차이점

인덱서와 속성은 개념에서 매우 유사하지만 다음과 같은 방법이 다릅니다.

  • 속성은 이름으로 식별되는 반면 인덱서는 해당 서명으로 식별됩니다.
  • 속성은 simple_name(§12.8.4) 또는 member_access(§12.8.7)를 통해 액세스하는 반면 인덱서 요소는 element_access(§12.8.12.3)를 통해 액세스됩니다.
  • 속성은 정적 멤버일 수 있지만 인덱서는 항상 인스턴스 멤버입니다.
  • 속성의 get 접근자는 매개 변수가 없는 메서드에 해당하지만 인덱서의 get 접근자는 인덱서와 동일한 매개 변수 목록을 가진 메서드에 해당합니다.
  • 속성의 set 접근자는 이름이 value단일 매개 변수인 메서드에 해당하지만 인덱서의 set 접근자는 인덱서와 동일한 매개 변수 목록과 명명된 value추가 매개 변수를 가진 메서드에 해당합니다.
  • 인덱서 접근자가 인덱서 매개 변수와 이름이 같은 지역 변수 또는 로컬 상수를 선언하는 것은 컴파일 시간 오류입니다.
  • 재정의된 속성 선언에서 상속된 속성은 base.P 구문을 사용하여 액세스합니다. 여기서 P은 속성 이름을 나타냅니다. 재정의 인덱서 선언에서 상속된 인덱서는 base[E] 구문을 사용하여 액세스하며, 여기서 E는 쉼표로 구분된 식 목록입니다.
  • "자동으로 구현된 인덱서"라는 개념은 없습니다. 세미콜론 accessor_body있는 비 추상적 외부 인덱서가 있는 것은 오류입니다.

이러한 차이점 외에도 §15.7.3, §15.7.5§15.7.6에 정의된 모든 규칙은 인덱서 접근자 및 속성 접근자에 적용됩니다.

§15.7.3, §15.7.5§15.7.6을 읽을 때 속성/속성을 인덱서/인덱서로 바꾸는 것은 정의된 용어에도 적용됩니다. 특히 읽기-쓰기 속성은 읽기/쓰기 인덱서가 되고, 읽기 전용 속성은 읽기 전용 인덱서가 되고, 쓰기 전용 속성은 쓰기 전용 인덱서가 됩니다.

15.10 연산자

15.10.1 일반

연산자는 클래스의 인스턴스에 적용할 수 있는 식 연산자의 의미를 정의하는 멤버입니다. 연산자는 operator_declaration사용하여 선언됩니다.

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

참고: 접두사 논리 부정(§12.9.4) 및 접두사 null 용서 연산자(§12.8.9)는 동일 어휘 토큰(!)으로 표현되지만 고유합니다. 후자는 오버로드 가능한 연산자가 아닙니다. 끝 메모

오버로드 가능한 연산자의 세 가지 범주는 단항 연산자(§15.10.2), 이진 연산자(§15.10.3) 및 변환 연산자(§15.10.4)입니다.

operator_body 세미콜론, 블록 본문(§15.6.1) 또는 식 본문(§15.6.1)입니다. 블록 본문은 연산자를 호출할 때 실행할 문을 지정하는 블록으로 구성됩니다. 블록§15.6.11설명된 값 반환 방법에 대한 규칙을 준수해야 합니다. 식 본문은 => 다음에 식과 세미콜론으로 구성되며, 연산자가 호출될 때 수행할 단일 식을 나타냅니다.

연산자의 경우 extern operator_body 단순히 세미콜론으로 구성됩니다. 다른 모든 연산자의 경우 operator_body 블록 본문 또는 식 본문입니다.

다음 규칙은 모든 연산자 선언에 적용됩니다.

  • 연산자 선언에는 publicstatic 수식자가 모두 포함되어야 합니다.
  • 연산자 in의 매개 변수에는 다른 수식어가 없어야 합니다.
  • 연산자의 서명(§15.10.2, §15.10.3, §15.10.4)은 동일한 클래스에 선언된 다른 모든 연산자의 서명과 다릅니다.
  • 연산자 선언에서 참조되는 모든 형식은 적어도 연산자 자체(§7.5.5)만큼 액세스할 수 있어야 합니다.
  • 동일한 한정자가 연산자 선언에 여러 번 표시되는 것은 오류입니다.

각 연산자 범주는 다음 하위 항목에 설명된 대로 추가 제한을 적용합니다.

다른 멤버와 마찬가지로 기본 클래스에 선언된 연산자는 파생 클래스에서 상속됩니다. 연산자 선언에는 연산자가 연산자의 서명에 참여하도록 선언된 클래스 또는 구조체가 항상 필요하므로 파생 클래스에 선언된 연산자가 기본 클래스에 선언된 연산자를 숨기는 것은 불가능합니다. new 따라서 연산자 선언에서는 한정자가 필요하지 않으므로 허용되지 않습니다.

단항 및 이진 연산자에 대한 추가 정보는 §12.4에서 찾을 수 있습니다.

변환 연산자에 대한 추가 정보는 §10.5에서 찾을 수 있습니다.

15.10.2 단항 연산자

다음 규칙은 연산자 T 선언을 포함하는 클래스 또는 구조체의 인스턴스 형식을 나타내는 단항 연산자 선언에 적용됩니다.

  • 단항 +, -, ! (논리 부정만 해당) 또는 ~ 연산자는 T 또는 T? 형식의 단일 매개 변수를 받아 어떤 형식으로든 반환할 수 있습니다.
  • 단항 ++ 또는 -- 연산자는 형식 T 의 단일 매개 변수를 취하거나 T? 동일한 형식 또는 파생된 형식을 반환해야 합니다.
  • 단항 true 또는 false 연산자는 T 또는 T? 형식의 단일 매개 변수를 받아 bool 형식을 반환해야 합니다.

단항 연산자의 서명은 연산자 토큰(+,, -, !, ~, ++--true또는false) 및 단일 매개 변수의 형식으로 구성됩니다. 반환 형식은 단항 연산자의 서명에 속하지 않으며 매개 변수의 이름도 아닙니다.

true 단항 연산자와 false 단항 연산자는 쌍 단위 선언이 필요합니다. 클래스가 다른 연산자를 선언하지 않고 이러한 연산자 중 하나를 선언하는 경우 컴파일 시간 오류가 발생합니다. truefalse 연산자는 §12.24자세히 설명되어 있습니다.

예제: 다음 예제에서는 정수 벡터 클래스에 대한 operator++의 구현 및 후속 사용을 보여 줍니다.

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // Read-write indexer

    public static IntVector operator++(IntVector iv)
    {
        IntVector temp = new IntVector(iv.Length);
        for (int i = 0; i < iv.Length; i++)
        {
            temp[i] = iv[i] + 1;
        }
        return temp;
    }
}

class Test
{
    static void Main()
    {
        IntVector iv1 = new IntVector(4); // Vector of 4 x 0
        IntVector iv2;
        iv2 = iv1++;              // iv2 contains 4 x 0, iv1 contains 4 x 1
        iv2 = ++iv1;              // iv2 contains 4 x 2, iv1 contains 4 x 2
    }
}

연산자 메서드는 접두사 증가 및 감소 연산자(§12.8.16) 및 접두사 증가 및 감소 연산자(§12.9.6)와 마찬가지로 피연산자에 1을 추가하여 생성된 값을 반환하는 방법을 확인합니다. C++와 달리 이 메서드는 후위 증가 연산자의 표준 의미 체계(§12.8.16)를 위반하므로 피연산자의 값을 직접 수정해서는 안 됩니다.

끝 예제

15.10.3 이진 연산자

다음 규칙은 연산자 T 선언을 포함하는 클래스 또는 구조체의 인스턴스 형식을 나타내는 이진 연산자 선언에 적용됩니다.

  • 이진 비시프트 연산자는 두 개의 매개 변수를 가져야 하며, 그 중 하나 이상은 T 또는 T? 형식이어야 하며, 반환 타입은 아무 것이나 가능합니다.
  • 이진 << 또는 >> 연산자(§12.11)는 두 개의 매개 변수를 사용하며, 그 중 첫 번째는 형식 T 또는 T?, 두 번째 매개 변수는 형식 int 또는 int?형식을 가지며 모든 형식을 반환할 수 있습니다.

이진 연산자의 서명은 연산자 토큰(+, ,-, *, /, %&, |^<<>>==!=><>=또는<=) 및 두 매개 변수의 형식으로 구성됩니다. 반환 형식 및 매개 변수 이름은 이진 연산자의 서명에 포함되지 않습니다.

특정 이진 연산자는 쌍 단위 선언이 필요합니다. 쌍의 모든 연산자 선언에 대해 쌍의 다른 연산자에 대한 일치 선언이 있어야 합니다. 반환 형식과 해당 매개 변수 형식 간에 ID 변환이 존재하는 경우 두 연산자 선언이 일치합니다. 다음 연산자는 쌍 단위 선언이 필요합니다.

  • 연산자 == 및 연산자 !=
  • 연산자 > 및 연산자 <
  • 연산자 >= 및 연산자 <=

15.10.4 변환 연산자

변환 연산자 선언은 미리 정의된 암시적 및 명시적 변환을 보강하는 사용자 정의 변환 (§10.5)을 도입합니다.

키워드를 포함하는 변환 연산자 선언은 implicit 사용자 정의 암시적 변환을 도입합니다. 암시적 변환은 함수 멤버 호출, 캐스트 식 및 할당을 비롯한 다양한 상황에서 발생할 수 있습니다. 이 내용은 §10.2에 자세히 설명되어 있습니다.

키워드를 포함하는 변환 연산자 선언은 explicit 사용자 정의 명시적 변환을 도입합니다. 명시적 변환은 캐스트 식에서 발생할 수 있으며 §10.3자세히 설명되어 있습니다.

변환 연산자는 변환 연산자의 매개 변수 형식으로 표시된 원본 형식에서 변환 연산자의 반환 형식으로 표시된 대상 형식으로 변환합니다.

지정된 원본 형식 S 및 대상 형식 T에서, 만약 S 또는 T이 null 허용 값 형식이라면, S₀T₀는 해당 기본 형식을 참조합니다. 그렇지 않으면, S₀T₀는 각각 ST와 같습니다. 클래스 또는 구조체는 다음이 모두 true인 경우에만 원본 형식에서 대상 형식 ST 으로의 변환을 선언할 수 있습니다.

  • S₀T₀ 서로 다른 형식입니다.

  • S₀ T₀ 또는 연산자 선언을 포함하는 클래스 또는 구조체의 인스턴스 형식입니다.

  • S₀T₀interface_type가 아닙니다.

  • 사용자 정의 변환을 제외하면, S에서 T로 또는 T에서 S로의 변환이 없습니다.

이러한 규칙의 목적을 위해 다른 형식과 상속 관계가 없는 고유 형식과 S 연결되거나 T 고유한 형식으로 간주되며 해당 형식 매개 변수에 대한 제약 조건은 무시됩니다.

: 다음에서:

class C<T> {...}

class D<T> : C<T>
{
    public static implicit operator C<int>(D<T> value) {...}     // Ok
    public static implicit operator C<string>(D<T> value) {...}  // Ok
    public static implicit operator C<T>(D<T> value) {...}       // Error
}

처음 두 연산자 선언은 각각 관계가 없는 고유한 형식으로 간주되기 때문에 Tintstring허용됩니다. 그러나 세 번째 연산자는 C<T>이(가) D<T>의 기본 클래스이므로 오류입니다.

끝 예제

두 번째 규칙에 따르면 변환 연산자는 연산자가 선언된 클래스 또는 구조체 형식으로 변환하거나 그 형식에서 변환해야 합니다.

예시: 클래스 또는 구조체 형식이 C에서 C로의 변환과 int에서 int로의 변환을 정의할 수 있지만, C에서 int로의 변환은 정의할 수 없습니다. 끝 예제

미리 정의된 변환을 직접 다시 정의할 수 없습니다. 따라서 object와 다른 모든 형식 간에는 이미 암시적 및 명시적 변환이 존재하기 때문에 변환 연산자를 사용하여 object에서 또는 object로 변환할 수 없습니다. 마찬가지로 변환이 이미 존재하기 때문에 변환의 원본 형식이나 대상 형식은 다른 변환의 기본 형식일 수 없습니다. 그러나 특정 형식 인수에 대해 미리 정의된 변환으로 이미 존재하는 변환을 지정하는 제네릭 형식에 대한 연산자를 선언할 수 있습니다.

예제:

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

형식 object이 형식 인수 T로 지정될 때, 두 번째 연산자는 이미 존재하는 변환(암시적 변환 및 따라서 모든 형식에서 형식 개체로의 명시적 변환)을 선언합니다.

끝 예제

두 형식 간에 미리 정의된 변환이 존재하는 경우 해당 형식 간의 사용자 정의 변환은 무시됩니다. 특별한 사항

  • 미리 정의된 암시적 변환(§10.2)이 S 유형에서 T 유형으로 존재하는 경우, S에서 T로의 모든 사용자 정의 변환(암시적 또는 명시적)은 무시됩니다.
  • 미리 정의된 명시적 변환(§10.3)이 형에서 S 형으로 존재하는 경우, T에서 S로의 사용자 정의 명시적 변환은 무시됩니다. 더욱이:
    • S 또는 T이 인터페이스 형식인 경우 S에서 T로의 사용자 정의 암시적 변환은 무시됩니다.
    • 그렇지 않으면 S에서 T로의 사용자 정의 암시적 변환도 여전히 고려됩니다.

object을 제외한 모든 형식에 대해 위의 Convertible<T>형식으로 선언된 연산자는 미리 정의된 변환과 충돌하지 않습니다.

예제:

void F(int i, Convertible<int> n)
{
    i = n;                    // Error
    i = (int)n;               // User-defined explicit conversion
    n = i;                    // User-defined implicit conversion
    n = (Convertible<int>)i;  // User-defined implicit conversion
}

그러나 형식 object의 경우 미리 정의된 변환은 다음을 제외한 모든 경우에 사용자 정의 변환을 숨깁니다.

void F(object o, Convertible<object> n)
{
    o = n;                       // Pre-defined boxing conversion
    o = (object)n;               // Pre-defined boxing conversion
    n = o;                       // User-defined implicit conversion
    n = (Convertible<object>)o;  // Pre-defined unboxing conversion
}

끝 예제

사용자 정의 변환은 interface_type으로부터 또는 interface_type로의 변환이 허용되지 않습니다. 특히 이 제한은 interface_type으로 변환할 때 사용자 정의 변환이 발생하지 않도록 보장하며, 변환이 성공하기 위해서는 변환되는 가 지정된 object을 실제로 구현하고 있어야 합니다.

변환 연산자의 서명은 원본 형식과 대상 형식으로 구성됩니다. (이 형식은 반환 형식이 서명에 참여하는 유일한 멤버 형식입니다.) 변환 연산자의 암시적 또는 명시적 분류는 연산자의 서명에 포함되지 않습니다. 따라서 클래스 또는 구조체는 소스 및 대상 형식이 동일한 암시적 변환 연산자와 명시적 변환 연산자를 모두 선언할 수 없습니다.

참고: 일반적으로 사용자 정의 암시적 변환은 예외를 throw하지 않으며 정보를 잃지 않도록 설계되어야 합니다. 사용자 정의 변환으로 인해 예외가 발생하거나(예: 원본 인수가 범위를 벗어났기 때문에) 정보가 손실될 수 있는 경우(예: 상위 비트 삭제) 해당 변환은 명시적 변환으로 정의되어야 합니다. 끝 메모

예제: 다음 코드에서

public struct Digit
{
    byte value;

    public Digit(byte value)
    {
        if (value < 0 || value > 9)
        {
            throw new ArgumentException();
        }
        this.value = value;
    }

    public static implicit operator byte(Digit d) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

Digit에서 byte로의 변환은 절대 예외를 발생시키거나 정보를 손실하지 않기 때문에 암시적입니다. 하지만 byte에서 Digit로의 변환은 명시적입니다. Digit은(는) 가능한 byte의 값 중 일부만을 나타낼 수 있기 때문입니다.

끝 예제

15.11 인스턴스 생성자

15.11.1 일반

인스턴스 생성자는 클래스의 인스턴스를 초기화하는 데 필요한 작업을 구현하는 멤버입니다. 인스턴스 생성자는 constructor_declaration사용하여 선언됩니다.

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

constructor_declaration 특성 집합(§22), 허용된 종류의 선언된 접근성(§15.3.6) 및 extern (§15.6.8) 한정자가 포함될 수 있습니다. 생성자 선언은 동일한 한정자를 여러 번 포함할 수 없습니다.

constructor_declarator 식별자는인스턴스 생성자가 선언된 클래스의 이름을 지정해야 합니다. 다른 이름을 지정하면 컴파일 시간 오류가 발생합니다.

인스턴스 생성자의 선택적 parameter_list 메서드의 parameter_list(§15.6)와 동일한 규칙이 적용됩니다. this 매개 변수의 한정자는 확장 메서드(§15.6.10)에만 적용됩니다. 생성자의 parameter_list 매개 변수에는 한정자가 포함되지 this 않습니다. 매개 변수 목록은 인스턴스 생성자의 서명(§7.6)을 정의하고 오버로드 확인(§12.6.4)이 호출에서 특정 인스턴스 생성자를 선택하는 프로세스를 제어합니다.

인스턴스 생성자의 parameter_list 참조되는 각 형식은 적어도 생성자 자체(§7.5.5)만큼 액세스할 수 있어야 합니다.

선택적으로 constructor_initializer는 이 인스턴스 생성자의 constructor_body에 주어진 문을 실행하기 전에 호출할 다른 인스턴스 생성자를 지정합니다. 이 내용은 §15.11.2에 자세히 설명되어 있습니다.

생성자 선언에 한 extern 정자가 포함된 경우 생성자는 외부 생성자라고 합니다. 외부 생성자 선언은 실제 구현을 제공하지 않으므로 해당 constructor_body 세미콜론으로 구성됩니다. 다른 모든 생성자의 경우 constructor_body 둘 중 하나로 구성됩니다.

  • 클래스의 새 인스턴스를 초기화하는 문을 지정하는 블록입니다.
  • 식 본문은 => 뒤에 과 세미콜론이 따라오며, 클래스의 새 인스턴스를 초기화하기 위한 단일 식을 나타냅니다.

constructor_body블록 또는 표현식 본문일 때는 반환 형식(void§15.6.11)이 있는 인스턴스 메서드의 블록에 정확히 해당합니다.

인스턴스 생성자는 상속되지 않습니다. 따라서 클래스에는 클래스에 인스턴스 생성자 선언이 없는 경우 기본 인스턴스 생성자가 자동으로 제공된다는 점을 제외하고 클래스에 실제로 선언된 생성자 이외의 인스턴스 생성자가 없습니다(§15.11.5).

인스턴스 생성자는 object_creation_expression(§12.8.17.2) 및 constructor_initializer통해 호출됩니다.

15.11.2 생성자 이니셜라이저

클래스를 object제외한 모든 인스턴스 생성자는 constructor_body 바로 앞에 다른 인스턴스 생성자의 호출을 암시적으로 포함합니다. 암시적으로 호출할 생성자는 constructor_initializer 의해 결정됩니다.

  • 양식 base(argument_list) (여기서 argument_list는 선택 사항임)을 사용하면 직접 기본 클래스의 인스턴스 생성자가 호출됩니다. 해당 생성자는 argument_list 및 §12.6.4오버로드 확인 규칙을 사용하여 선택됩니다. 후보 인스턴스 생성자 집합은 직접 기본 클래스의 액세스 가능한 모든 인스턴스 생성자로 구성됩니다. 이 집합이 비어 있거나 단일 최상의 인스턴스 생성자를 식별할 수 없는 경우 컴파일 시간 오류가 발생합니다.
  • 양식 this(argument_list) 인스턴스 생성자 이니셜라이저( argument_list 선택 사항)는 동일한 클래스에서 다른 인스턴스 생성자를 호출합니다. 생성자는 argument_list 및 §12.6.4오버로드 확인 규칙을 사용하여 선택됩니다. 후보 인스턴스 생성자 집합은 클래스 자체에 선언된 모든 인스턴스 생성자로 구성됩니다. 적용 가능한 인스턴스 생성자의 결과 집합이 비어 있거나 단일 최상의 인스턴스 생성자를 식별할 수 없는 경우 컴파일 시간 오류가 발생합니다. 인스턴스 생성자 선언이 하나 이상의 생성자 이니셜라이저 체인을 통해 자신을 호출하는 경우 컴파일 시간 오류가 발생합니다.

인스턴스 생성자에 생성자 이니셜라이저가 없는 경우 폼 base() 의 생성자 이니셜라이저가 암시적으로 제공됩니다.

참고: 따라서 형태의 인스턴스 생성자 선언은 다음과 같습니다.

C(...) {...}

정확히 같다

C(...) : base() {...}

끝 메모

인스턴스 생성자 선언의 parameter_list 지정된 매개 변수의 범위에는 해당 선언의 생성자 이니셜라이저가 포함됩니다. 따라서 생성자 이니셜라이저는 생성자의 매개 변수에 액세스할 수 있습니다.

예제:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

끝 예제

인스턴스 생성자 이니셜라이저는 생성되는 인스턴스에 액세스할 수 없습니다. 따라서 simple_name을 통해 인스턴스 멤버를 참조하는 인수 식은 컴파일 시간 오류이므로, 생성자 이니셜라이저의 인수 식에서 this를 참조하는 것은 컴파일 시간 오류입니다.

15.11.3 인스턴스 변수 이니셜라이저

외부에 없는 인스턴스 생성자가 생성자 이니셜라이저를 가지지 않거나 형식 base(...)의 생성자 이니셜라이저를 가지는 경우, 해당 생성자는 클래스에 선언된 인스턴스 필드에 대해 variable_initializer에 의해 지정된 초기화를 암시적으로 수행합니다. 이는 생성자에 진입할 때와 직접 기본 클래스 생성자의 암시적 호출 전에 즉시 실행되는 할당 시퀀스에 해당합니다. 변수 이니셜라이저는 클래스 선언(§15.5.6)에 표시되는 텍스트 순서로 실행됩니다.

extern 인스턴스 생성자가 변수 이니셜라이저를 실행할 필요는 없습니다.

15.11.4 생성자 실행

변수 이니셜라이저는 대입 문으로 변환되며, 이러한 할당 문은 기본 클래스 인스턴스 생성자를 호출하기 전에 실행됩니다. 이 순서를 지정하면 해당 인스턴스에 액세스할 수 있는 문이 실행되기 전에 모든 인스턴스 필드가 변수 이니셜라이저에 의해 초기화됩니다.

: 다음을 지정합니다.

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

    public override void PrintFields() =>
        Console.WriteLine($"x = {x}, y = {y}");
}

new B()B의 인스턴스를 생성할 때, 다음과 같은 출력이 생성됩니다.

x = 1, y = 0

기본 클래스 인스턴스 생성자가 호출되기 전에 변수 이니셜라이저가 실행되기 때문에 값 x 은 1입니다. 기본 클래스 생성자가 반환된 후에야 y에 대한 할당이 실행되기 때문에 int의 값은 y의 기본값인 0입니다. 인스턴스 변수 이니셜라이저 및 생성자 이니셜라이저를 constructor_body 전에 자동으로 삽입되는 문으로 생각하는 것이 유용합니다. 예제

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

에는 여러 변수 이니셜라이저가 포함되어 있습니다. 또한 두 폼(basethis)의 생성자 이니셜라이저도 포함합니다. 이 예제는 아래 표시된 코드에 해당합니다. 여기서 각 주석은 자동으로 삽입된 문을 나타냅니다(자동으로 삽입된 생성자 호출에 사용되는 구문은 유효하지 않지만 메커니즘을 설명하기 위한 용도로만 사용됨).

class A
{
    int x, y, count;
    public A()
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = 0;
    }

    public A(int n)
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = n;
    }
}

class B : A
{
    double sqrt2;
    ArrayList items;
    int max;
    public B() : this(100)
    {
        B(100);                      // Invoke B(int) constructor
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        sqrt2 = Math.Sqrt(2.0);      // Variable initializer
        items = new ArrayList(100);  // Variable initializer
        A(n - 1);                    // Invoke A(int) constructor
        max = n;
    }
}

끝 예제

15.11.5 기본 생성자

클래스에 인스턴스 생성자 선언이 없는 경우 기본 인스턴스 생성자가 자동으로 제공됩니다. 이 기본 생성자는 폼 base()의 생성자 이니셜라이저가 있는 것처럼 직접 기본 클래스의 생성자를 호출합니다. 클래스가 추상인 경우 기본 생성자에 대해 선언된 접근성이 보호됩니다. 그렇지 않으면 기본 생성자에 대해 선언된 접근성이 public입니다.

참고: 따라서 기본 생성자는 항상 형태입니다.

protected C(): base() {}

또는

public C(): base() {}

클래스의 이름은 C />입니다.

끝 메모

오버로드 확인에서 기본 클래스 생성자 이니셜라이저에 가장 적합한 고유한 후보를 확인할 수 없는 경우 컴파일 시간 오류가 발생합니다.

예제: 다음 코드에서

class Message
{
    object sender;
    string text;
}

클래스에 인스턴스 생성자 선언이 없기 때문에 기본 생성자가 제공됩니다. 따라서 이 예제는 정확하게 등가입니다.

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

끝 예제

15.12 정적 생성자

정적 생성자는 닫힌 클래스를 초기화하는 데 필요한 작업을 구현하는 멤버입니다. 정적 생성자는 static_constructor_declaration사용하여 선언됩니다.

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

static_constructor_declaration은 특성(§22) 및 한 수정자(extern)를 포함할 수 있습니다.

static_constructor_declaration 식별자는정적 생성자가 선언되는 클래스의 이름을 지정해야 합니다. 다른 이름을 지정하면 컴파일 시간 오류가 발생합니다.

정적 생성자 선언에 한 extern 정자가 포함된 경우 정적 생성자는 외부 정적 생성자라고 합니다. 외부 정적 생성자 선언은 실제 구현을 제공하지 않으므로 해당 static_constructor_body 세미콜론으로 구성됩니다. 다른 모든 정적 생성자 선언의 경우 static_constructor_body 둘 중 하나로 구성됩니다.

  • 클래스를 초기화하기 위해 실행할 문을 지정하는 블록입니다.
  • 식 본문은 => 뒤에 과 세미콜론으로 구성되며, 클래스를 초기화하기 위해 실행할 단일 식을 나타냅니다.

static_constructor_body블록 또는 식 본문일 경우 이는 반환 형식을 가진 정적 메서드의 void와 정확히 일치합니다 (§15.6.11).

정적 생성자는 상속되지 않으며 직접 호출할 수 없습니다.

닫힌 클래스에 대한 정적 생성자는 지정된 애플리케이션 도메인에서 한 번만 실행됩니다. 정적 생성자의 실행은 애플리케이션 도메인 내에서 발생할 다음 이벤트 중 첫 번째에 의해 트리거됩니다.

  • 클래스의 인스턴스가 만들어집니다.
  • 클래스의 모든 정적 멤버가 참조됩니다.

클래스에 실행이 Main 시작되는 메서드(§7.1)가 포함되어 있으면 메서드가 호출되기 전에 해당 클래스의 Main 정적 생성자가 실행됩니다.

새 닫힌 클래스 형식을 초기화하려면 먼저 해당 특정 닫힌 형식에 대한 새 정적 필드 집합(§15.5.2)을 만들어야 합니다. 각 정적 필드는 기본값(§15.5.5)으로 초기화되어야 합니다. 다음에 이어서:

  • 정적 생성자가 없거나 외부 static이 아닌 정적 생성자가 없는 경우 다음을 수행합니다.
    • 정적 필드 이니셜라이저(§15.5.6.2)는 해당 정적 필드에 대해 실행되어야 합니다.
    • 그러면 extern이 아닌 정적 생성자(있는 경우)가 실행됩니다.
  • 그렇지 않으면 extern 정적 생성자가 있는 경우 실행되어야 합니다. 정적 변수 이니셜라이저는 extern 정적 생성자에 의해 실행될 필요가 없습니다.

: 예제

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

출력물을 생성해야 합니다.

Init A
A.F
Init B
B.F

A의 정적 생성자 실행은 A.F 호출에 의해 트리거되며, B의 정적 생성자 실행은 B.F 호출에 의해 트리거됩니다.

끝 예제

변수 이니셜라이저가 있는 정적 필드가 기본값 상태에서 관찰될 수 있도록 하는 순환 종속성을 생성할 수 있습니다.

: 예제

class A
{
    public static int X;

    static A()
    {
        X = B.Y + 1;
    }
}

class B
{
    public static int Y = A.X + 1;

    static B() {}

    static void Main()
    {
        Console.WriteLine($"X = {A.X}, Y = {B.Y}");
    }
}

는 출력을 생성합니다.

X = 1, Y = 2

메서드를 Main 실행하기 위해 시스템은 먼저 클래스B.Y의 정적 생성자 이전의 이니셜라이저B를 실행합니다. Y의 초기화로 인해 A의 값이 참조되어 staticA.X 생성자가 실행됩니다. 정적 생성자는 A 차례로 값 X계산을 진행하고, 이 경우 기본값 Y인 0을 가져옵니다. A.X 은 1로 초기화됩니다. 그러면 '의 정적 필드 이니셜라이저 및 정적 생성자를 실행하는 A프로세스가 완료되고 초기 값의 Y계산으로 돌아가면 결과는 2가 됩니다.

끝 예제

정적 생성자는 닫힌 생성된 각 클래스 형식에 대해 정확히 한 번 실행되므로 제약 조건(§15.2.5)을 통해 컴파일 타임에 확인할 수 없는 형식 매개 변수에 대해 런타임 검사를 적용하는 것이 편리합니다.

: 다음 형식은 정적 생성자를 사용하여 형식 인수가 열거형임을 적용합니다.

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

끝 예제

15.13 파이널라이저

참고: 이 사양의 이전 버전에서는 현재 "종료자"라고 하는 것을 "소멸자"라고 합니다. 경험에 따르면 "소멸자"라는 용어는 혼동을 일으켰으며, 특히 C++를 아는 프로그래머에게 잘못된 기대치를 초래하는 경우가 많습니다. C++에서는 소멸자가 결정적인 방식으로 호출되는 반면 C#에서는 종료자가 호출되지 않습니다. C#에서 명확한 동작을 얻으려면 Dispose를 사용해야 합니다. 끝 메모

종료자는 클래스의 인스턴스를 종결하는 데 필요한 작업을 구현하는 멤버입니다. 종료자는 `finalizer_declaration`을 사용하여 선언됩니다.

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

finalizer_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier(§23.2)는 안전하지 않은 코드(§23)에서만 사용할 수 있습니다.

finalizer_declaration 특성 집합(§22)을 포함할 수 있습니다.

finalizer_declarator 식별자는종료자가 선언된 클래스의 이름을 지정해야 합니다. 다른 이름을 지정하면 컴파일 시간 오류가 발생합니다.

최종자 선언에 extern 수정자가 포함되어 있으면, 그 최종자는 외부 최종자라고 합니다. 외부 종료자 선언은 실제 구현을 제공하지 않으므로 해당 finalizer_body 세미콜론으로 구성됩니다. 다른 모든 종료자의 경우 finalizer_body는 둘 중 하나로 구성되어 있습니다.

  • 클래스의 인스턴스를 완료하기 위해 실행할 문을 지정하는 블록입니다.
  • 식 본문 또는 식 본문은 와 그 뒤의 표현 과 세미콜론으로 구성되며, 클래스 인스턴스를 완료하기 위해 실행해야 하는 단일 식을 나타냅니다.

finalizer_body블록 또는 식 본문인 경우, 이는 반환 형식 (void)이 있는 인스턴스 메서드의 method_body와 정확히 일치합니다.

종료자는 상속되지 않습니다. 따라서 클래스에는 해당 클래스에서 선언될 수 있는 종료자 외에는 다른 종료자가 없습니다.

참고: 종료자에는 매개 변수가 없어야 하므로 오버로드할 수 없으므로 클래스에는 최대 하나의 종료자가 있을 수 있습니다. 끝 메모

종료자는 자동으로 호출되며 명시적으로 호출할 수 없습니다. 더 이상 코드에서 해당 인스턴스를 사용할 수 없으면 인스턴스를 종료할 수 있게 됩니다. 인스턴스에 대한 종료자 실행은 인스턴스가 종료될 수 있게 된 후 언제든지 발생할 수 있습니다(§7.9). 인스턴스가 종료되면 해당 인스턴스의 상속 체인에 있는 종료자가 대부분의 파생에서 최소 파생으로 순서대로 호출됩니다. 종료자는 모든 스레드에서 실행될 수 있습니다. 종료자가 실행되는 시기와 방법을 제어하는 규칙에 대한 자세한 내용은 §7.9를 참조하세요.

예제: 예제의 출력

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

이다

B's finalizer
A's finalizer

상속 체인의 파이널라이저는 가장 파생된 것부터 가장 덜 파생된 것 순서로 호출되기 때문에

끝 예제

종료자는 가상 메서드 FinalizeSystem.Object에서 재정의하여 구현됩니다. C# 프로그램은 이 메서드를 직접 재정의하거나 그 재정의를 호출할 수 없습니다.

: 예를 들어 프로그램

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

에는 두 개의 오류가 포함됩니다.

끝 예제

컴파일러는 이 메서드와 재정의가 전혀 없는 것처럼 동작해야 합니다.

: 따라서 이 프로그램은 다음과 같습니다.

class A
{
    void Finalize() {}  // Permitted
}

"System.Object가 유효하며, 표시된 메서드가 Finalize의 메서드를 숨깁니다."

끝 예제

종료자에서 예외가 throw되는 경우 동작에 대한 자세한 내용은 §21.4를 참조하세요.

15.14 비동기 함수

15.14.1 일반

한정자 가 있는 메서드(§15.6) 또는 무명 함수(§12.19)는 비동기 함수라고 합니다. 일반적으로 비동기라는 용어는 async 한정자가 있는 모든 종류의 함수를 설명하는 데 사용됩니다.

비동기 함수의 매개변수 목록에 in, out, ref 파라미터나 ref struct 타입의 어떤 파라미터라도 지정하는 것은 컴파일 시간 오류입니다.

비동기 메서드의 return_typevoid 또는 비동기 반복기 형식(§15.15)이어야 합니다. 결과 값을 생성하는 비동기 메서드의 경우 작업 유형 또는 비동기 반복기 형식(§15.15.3)이 제네릭이어야 합니다. 결과 값을 생성하지 않는 비동기 메서드의 경우 작업 유형은 제네릭이 아니어야 합니다. 이러한 유형은 이 사양에서 각각 «TaskType»<T>«TaskType»로 참조됩니다. 표준 라이브러리 형식 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>System.Threading.Tasks.ValueTask<T>에서 생성된 형식은 작업 형식으로, 특성을 통해 과 연결된 클래스, 구조체, 또는 인터페이스 형식입니다. 이러한 형식은 이 사양에서 다음과 같이 «TaskBuilderType»<T> 참조됩니다 «TaskBuilderType». 작업 형식에는 최대 하나의 형식 매개 변수가 있을 수 있으며 제네릭 형식에 중첩될 수 없습니다.

작업 유형을 반환하는 비동기 메서드는 작업 반환이라고 합니다.

작업 유형은 정확한 정의에 따라 달라질 수 있지만 언어의 관점에서 작업 유형은 불완전하거나 성공했거나 오류가 발생한 상태 중 하나입니다. 오류가 발생한 작업은 관련 예외를 기록합니다. 성공하면 «TaskType»<T> 형식T의 결과가 기록됩니다. 작업 형식은 대기할 수 있으므로 작업은 await 식의 피연산자일 수 있습니다(§12.9.8).

: 작업 유형 MyTask<T> 은 작업 작성기 유형 및 awaiter 형식 MyTaskMethodBuilder<T>Awaiter<T>과 연결됩니다.

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

끝 예제

작업 작성기 형식은 특정 작업 유형(§15.14.2)에 해당하는 클래스 또는 구조체 형식입니다. 작업 작성기 유형은 해당 작업 유형의 선언된 접근성과 정확히 일치해야 합니다.

참고: 작업 유형이 선언된 internal경우 해당 작성기 형식도 선언되고 internal 동일한 어셈블리에 정의되어야 합니다. 작업 유형이 다른 형식 내에 중첩된 경우 작업 부이더 형식도 동일한 형식으로 중첩되어야 합니다. 끝 메모

비동기 함수는 본문에서 await 식(§12.9.8)을 통해 평가를 일시 중단하는 기능을 줍니다. 대리자를 통해 다시 시작하여 일시 중단 대기 식의 지점에서 평가를 재개할 수 있습니다. 재개 대리자는 형식 System.Action이며, 이것이 호출되면 비동기 함수 호출의 평가는 멈췄던 await 식에서 다시 시작됩니다. 비동기 함수 호출의 현재 호출자는 함수 호출이 일시 중단된 적이 없는 경우 원래 호출자이거나, 그렇지 않으면 다시 시작 대리자의 가장 최근 호출자입니다.

15.14.2 작업 유형 작성기 패턴

작업 작성기 형식에는 최대 하나의 형식 매개 변수가 있을 수 있으며 제네릭 형식에 중첩될 수 없습니다. 작업 작성기 유형에는 선언된 SetResult 접근성을 가진 다음 멤버(제네릭이 아닌 작업 작성기 형식의 public 경우 매개 변수가 없음)가 있어야 합니다.

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

컴파일러는 «TaskBuilderType»을 사용하여 비동기 함수의 평가를 일시 중단하고 다시 시작하기 위한 의미 체계를 구현하는 코드를 생성해야 합니다. 컴파일러는 다음과 같이 «TaskBuilderType»을 사용해야 합니다.

  • «TaskBuilderType».Create() 이 목록에 이름이 지정된 builder «TaskBuilderType»의 인스턴스를 만들기 위해 호출됩니다.
  • builder.Start(ref stateMachine) 는 컴파일러에서 생성된 상태 컴퓨터 인스턴스 stateMachine와 작성기를 연결하기 위해 호출됩니다.
    • 빌더는 상태 머신을 진행하기 위해 stateMachine.MoveNext()에서 또는 Start()가 반환된 후 Start()을 호출해야 합니다.
  • Start()가 반환된 후, async 메서드는 비동기 메서드에서 반환하기 위한 태스크를 위해 builder.Task를 호출합니다.
  • stateMachine.MoveNext()를 호출할 때마다 상태 기계가 진행됩니다.
  • 상태 머신이 성공적으로 완료되면 builder.SetResult()이 메서드 반환 값(있는 경우)을 사용하여 호출됩니다.
  • 그렇지 않으면, 상태 머신에서 e 예외가 throw될 때, builder.SetException(e)가 호출됩니다.
  • 상태 기계가 await expr 표현에 도달하면 expr.GetAwaiter()이 호출됩니다.
  • ICriticalNotifyCompletion가 구현되고 IsCompleted가 false가 되면 상태 기계가 builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)를 호출합니다.
    • AwaitUnsafeOnCompleted()는 awaiter가 완료될 때 awaiter.UnsafeOnCompleted(action)를 호출하는 Action와 함께 stateMachine.MoveNext()를 호출해야 합니다.
  • 그렇지 않으면 상태 기계가 builder.AwaitOnCompleted(ref awaiter, ref stateMachine)를 호출합니다.
    • AwaitOnCompleted()는 awaiter가 완료될 때 awaiter.OnCompleted(action)를 호출하는 Action와 함께 stateMachine.MoveNext()를 호출해야 합니다.
  • SetStateMachine(IAsyncStateMachine) 는 컴파일러 생성 IAsyncStateMachine 구현에 의해 호출되어 상태 컴퓨터 인스턴스와 연결된 작성기의 인스턴스를 식별할 수 있으며, 특히 상태 컴퓨터가 값 형식으로 구현되는 경우를 나타냅니다.
    • 작성기가 를 호출하면 는 연결된 작성기 인스턴스에서 를 호출할 것입니다. .

참고: 두 SetResult(T result)«TaskType»<T> Task { get; } 모두에 대해, 매개변수와 인수가 각각 T으로 동일성 변환 가능해야 합니다. 이렇게 하면 작업 유형 작성기에서 튜플과 같은 형식을 지원할 수 있습니다. 여기서 동일하지 않은 두 형식은 ID 변환이 가능합니다. 끝 메모

15.14.3 작업 반환 비동기 함수 평가

작업 반환 비동기 함수를 호출하면 반환된 작업 유형의 인스턴스가 생성됩니다. 이를 비동기 함수의 반환 작업 이라고 합니다. 작업은 처음에 불완전한 상태입니다.

그런 다음 비동기 함수 본문은 대기 식에 도달하여 일시 중단되거나 종료될 때까지 평가되며, 이때 반환 작업과 함께 지점 컨트롤이 호출자에게 반환됩니다.

비동기 함수의 본문이 종료되면 반환된 작업이 불완전한 상태에서 벗어납니다.

  • 함수 본문이 반환 문 또는 본문의 끝에 도달한 결과로 종료되는 경우 모든 결과 값이 반환 작업에 기록되어 성공 상태로 전환됩니다.
  • 함수 본문이 예외OperationCanceledException를 잡지 못해 종료되면, 해당 예외는 취소된 상태로 전환되는 반환 작업에 기록됩니다.
  • 함수 본문이 다른 처리가 안 된 예외(§13.10.6)로 인해 종료되는 경우, 해당 예외는 오류 상태에 있는 반환 작업에 기록됩니다.

15.14.4 값을 반환하지 않는 비동기 함수 평가

비동기 함수의 반환 형식인 경우 평가는 void다음과 같은 방식으로 위와 다릅니다. 작업이 반환되지 않으므로 함수는 대신 완료 및 예외를 현재 스레드의 동기화 컨텍스트에 전달합니다. 동기화 컨텍스트의 정확한 정의는 구현에 따라 달라지지만 현재 스레드가 실행되고 있는 "위치"의 표현입니다. void을 반환하는 비동기 함수의 평가가 시작되거나, 성공적으로 완료되거나, 처리되지 않은 예외가 throw될 때 동기화 컨텍스트는 알림을 받습니다.

이렇게 하면 컨텍스트에서 void을(를) 반환하는 비동기 함수의 수를 추적하고, 그 함수에서 나오는 예외를 어떻게 전파할지 결정할 수 있습니다.

15.15 동기 및 비동기 반복기

15.15.1 일반

반복기 블록(§13.3)을 사용하여 구현된 함수 멤버(§12.6) 또는 로컬 함수(§13.6.4)를 반복기라고 합니다. 반복기 블록은 해당 함수 멤버의 반환 형식이 열거자 인터페이스(§15.15.2) 또는 열거 가능한 인터페이스 중 하나(§15.15.3)인 한 함수 멤버의 본문으로 사용할 수 있습니다.

반복기 블록(§13.3)을 사용하여 구현된 비동기 함수(§15.14)를 비동기 반복기라고 합니다. 해당 함수 멤버의 반환 형식이 비동기 열거자 인터페이스(§15.15.2) 또는 비동기 열거 가능 인터페이스(§15.15.3)인 경우 비동기 반복기 블록을 함수 멤버의 본문으로 사용할 수 있습니다.

반복기 블록은 method_body, operator_body 또는 accessor_body 발생할 수 있지만 이벤트, 인스턴스 생성자, 정적 생성자 및 종료자는 동기 또는 비동기 반복기로 구현되지 않습니다.

반복자 블록을 사용하여 함수 멤버나 로컬 함수를 구현할 때, 함수 멤버의 매개 변수 목록에 in, out, 또는 ref 매개 변수를 지정하거나, ref struct 형식의 매개 변수를 지정하는 것은 컴파일 시간 오류입니다.

15.15.2 열거자 인터페이스

열거자 인터페이스는 제네릭 인터페이스가 아닌 인터페이스 및 제네릭 인터페이스 System.Collections.IEnumerator 의 모든 인스턴스화입니다System.Collections.Generic.IEnumerator<T>.

비동기 열거자 인터페이스는 모두 제네릭 인터페이스System.Collections.Generic.IAsyncEnumerator<T>의 인스턴스화입니다.

간단히 말해, 이 하위 조항과 그 형제 조항에서 이러한 인터페이스는 각각 IEnumerator, IEnumerator<T>, 및 IAsyncEnumerator<T>로 참조됩니다.

15.15.3 열거 가능한 인터페이스

열거 가능한 인터페이스는 제네릭 인터페이스가 아닌 인터페이스 및 제네릭 인터페이스 System.Collections.IEnumerable 의 모든 인스턴스화입니다System.Collections.Generic.IEnumerable<T>.

비동기 열거 가능 인터페이스는 모두 제네릭 인터페이스System.Collections.Generic.IAsyncEnumerable<T>의 인스턴스화입니다.

간단히 말해, 이 하위 조항과 그 형제 조항에서 이러한 인터페이스는 각각 IEnumerable, IEnumerable<T>, 및 IAsyncEnumerable<T>로 참조됩니다.

15.15.4 수익률 유형

반복기는 모두 동일한 형식의 값 시퀀스를 생성합니다. 이 형식을 반복기의 수율 형식 이라고 합니다.

  • 반복기가 IEnumerator 또는 IEnumerable을(를) 반환할 경우, 그 수율 유형은 object입니다.
  • 반복기가 IEnumerator<T>, IAsyncEnumerator<T>, IEnumerable<T>, 또는 IAsyncEnumerable<T>를 반환할 때 그 수율 유형은 T입니다.

15.15.5 열거형 개체

15.15.5.1 일반

반복기 블록을 사용하여 열거자 인터페이스 형식을 반환하는 함수 멤버 또는 로컬 함수를 구현하는 경우 함수를 호출해도 반복기 블록에서 코드를 즉시 실행하지는 않습니다. 대신 열거자 개체 가 만들어지고 반환됩니다. 이 개체는 반복기 블록에 지정된 코드를 캡슐화하고 반복기 블록의 코드 실행은 열거자 개체 MoveNext 또는 MoveNextAsync 메서드가 호출될 때 발생합니다. 열거자 개체의 특징은 다음과 같습니다.

  • 반복기는 System.IDisposable, IEnumeratorIEnumerator<T> 또는 System.IAsyncDisposableIAsyncEnumerator<T>를 구현하며, 여기서 T는 반복기의 수율 형식입니다.
  • 함수 멤버에 전달된 인수 값(있는 경우) 및 인스턴스 값의 복사본을 사용하여 초기화됩니다.
  • 이전, 실행, 일시 중단이후의 4가지 잠재적 상태가 있으며 처음에는 이전 상태에 있습니다.

열거자 개체는 일반적으로 반복기 블록의 코드를 캡슐화하고 열거자 인터페이스를 구현하는 컴파일러 생성 열거자 클래스의 인스턴스이지만 다른 구현 방법이 가능합니다. 컴파일러에서 열거자 클래스를 생성하는 경우 해당 클래스는 함수 멤버를 포함하는 클래스에서 직접 또는 간접적으로 중첩되고 프라이빗 접근성을 가지며 컴파일러 사용을 위해 예약된 이름(§6.4.3)을 갖습니다.

열거자 개체는 위에서 지정한 인터페이스보다 더 많은 인터페이스를 구현할 수 있습니다.

다음 하위클래스는 열거자를 진행시키고, 열거자에서 현재 값을 검색하고, 열거자가 사용하는 리소스를 삭제하는 데 필요한 멤버 동작을 설명합니다. 동기 및 비동기 열거자에 대해 각각 다음 멤버에 정의됩니다.

  • 열거자를 진행하려면 MoveNextMoveNextAsync를 합니다.
  • 현재 값을 검색하려면 : Current.
  • 리소스를 삭제하려면 다음을 수행합니다. DisposeDisposeAsync

열거자 개체는 메서드를 IEnumerator.Reset 지원하지 않습니다. 이 메서드를 호출하면 System.NotSupportedException 예외가 발생합니다.

동기 및 비동기 반복기 블록은 비동기 반복기 멤버가 작업 유형을 반환하고 대기할 수 있다는 점에서 다릅니다.

15.15.5.2 열거자 앞으로 이동

열거자 개체의 메서드 및 MoveNext 메서드는 MoveNextAsync 반복기 블록의 코드를 캡슐화합니다. MoveNext 또는 MoveNextAsync 메서드를 호출하면 반복기 블록에서 코드가 실행되고 열거자 객체의 Current 속성이 적절하게 설정됩니다.

MoveNextbool 아래에 설명된 의미를 갖는 값을 반환합니다. MoveNextAsyncValueTask<bool> (§15.14.3)을 반환합니다. 반환 MoveNextAsync 된 작업의 결과 값은 결과 값 MoveNext과 의미가 같습니다. 다음 설명에서 설명 MoveNext 된 작업은 다음 차이점과 함께 적용 MoveNextAsync 됩니다. 여기서 MoveNext 는 해당 작업을 반환 true 하거나 false, MoveNextAsync 작업을 완료된 상태로 설정하고, 작업의 결과 값을 해당 true 값 또는 false 값으로 설정합니다.

MoveNext 또는 MoveNextAsync가 수행하는 정확한 동작은 호출 시 열거자 객체의 상태에 따라 달라집니다.

  • 열거자 개체의 상태가 이전 상태인 경우 MoveNext 호출:
    • 상태를 실행 중으로 변경합니다.
    • 반복기 블록의 매개 변수(포함 this)를 열거자 개체가 초기화될 때 저장된 인수 값 및 인스턴스 값으로 초기화합니다.
    • 아래 설명된 대로 실행이 중단될 때까지 처음부터 반복기 블록을 실행합니다.
  • 열거자 개체의 상태가 실행 중이면 호출 MoveNext 결과가 지정되지 않습니다.
  • 열거자 개체 의 상태가 일시 중단된 경우 MoveNext를 호출합니다.
    • 상태를 실행 중으로 변경합니다.
    • 반복기 블록의 실행이 마지막으로 일시 중단되었을 때 저장된 값으로 모든 지역 변수 및 매개 변수(포함 this)의 값을 복원합니다.

      참고: 이러한 변수에서 참조하는 개체의 내용은 이전 호출 MoveNext이후 변경되었을 수 있습니다. 끝 메모

    • 실행 일시 중단을 발생시킨 yield return 문 바로 다음에 반복기 블록의 실행을 다시 시작하고 아래 설명된 대로 실행이 중단될 때까지 계속됩니다.
  • 열거자 개체의 상태가 이후인 경우, MoveNext를 호출하면 false를 반환합니다.

반복기 블록을 실행하면 MoveNext 문으로, yield return 문으로, 반복기 블록의 끝에 도달하여, 또는 반복기 블록에서 예외가 throw되고 전파되어 실행이 중단될 수 있습니다.

  • yield return 문이 발견되면(§9.4.4.20):
    • 문에 지정된 식은 계산되고 암시적으로 수율 형식으로 변환되며 열거자 개체의 속성에 Current 할당됩니다.
    • 반복기 본문의 실행이 일시 중단됩니다. 이 this 문의 위치와 마찬가지로 모든 지역 변수 및 매개 변수(포함yield return)의 값이 저장됩니다. yield return 문이 하나 이상의 try 블록 내에 있는 경우 연결된 최종 블록은 현재 실행되지 않습니다.
    • 열거자 개체의 상태가 일시 중단됨으로 변경됩니다.
    • 메서드는 호출자에게 MoveNext을 반환하여, 반복이 다음 값으로 성공적으로 진행되었음을 나타냅니다.
  • yield break 문이 발견되면(§9.4.4.20):
    • yield break 문이 하나 이상의 try 블록 내에 있으면 연결된 finally 블록이 실행됩니다.
    • 열거자 개체의 상태가 After변경됩니다.
    • MoveNext 메서드는 반복이 완료되었음을 나타내며 false을 호출자에게 반환합니다.
  • 반복기 본문의 끝이 발견되면 다음을 수행합니다.
    • 열거자 개체의 상태가 After변경됩니다.
    • MoveNext 메서드는 반복이 완료되었음을 나타내며 false을 호출자에게 반환합니다.
  • 예외가 발생하고 반복기 블록 밖으로 전파되는 경우:
    • 반복기 본문의 적절한 finally 블록은 예외 전파에 의해 실행됩니다.
    • 열거자 개체의 상태가 After변경됩니다.
    • 예외 전파는 메서드의 MoveNext 호출자에게 계속됩니다.

15.15.5.3 현재 값 검색

열거자 개체의 Current 속성은 반복기 블록의 yield return 구문에 의해 영향을 받습니다.

참고: 이 Current 속성은 동기 및 비동기 반복기 개체 모두에 대한 동기 속성입니다. 끝 메모

열거자 개체가 일시 중단된 상태인 경우 값 Current 은 이전 호출 MoveNext에서 설정한 값입니다. 열거자 개체가 이전, 실행 중 또는 이후 상태에 있는 경우 액세스 결과가 지정되지 않습니다.Current

object가 아닌 다른 수율 형식의 반복기의 경우, 열거자 객체의 Current 구현을 통해 IEnumerable에 접근하는 결과는, 열거자 객체의 Current 구현을 통해 IEnumerator<T>에 접근하여 그 결과를 object로 캐스팅하는 것과 같습니다.

15.15.5.4 리소스 삭제

Dispose 또는 DisposeAsync 메서드는 반복을 정리하기 위해 열거자 개체를 after 상태로 이동하는 데 사용됩니다.

  • 열거자 개체의 상태가 이전일 때 Dispose을(를) 호출하면 상태가 이후로 변경됩니다.
  • 열거자 개체의 상태가 실행 중이면 호출 Dispose 결과가 지정되지 않습니다.
  • 열거자 개체 의 상태가 일시 중단된 경우 다음을 호출합니다 Dispose.
    • 상태를 실행 중으로 변경합니다.
    • 마지막으로 실행된 yield return 문이 yield break 문인 것처럼 finally 블록을 실행합니다. 예외가 발생하고 반복기 본문 밖으로 전파되면, 열거자 개체의 상태는 로 설정되며, 메서드의 Dispose 호출자에게 예외가 전파됩니다.
    • 상태를 이후로 변경합니다.
  • 열거자 개체의 상태가 이후면 호출에 Dispose 영향을 주지 않습니다.

15.15.6 열거 가능한 개체

15.15.6.1 일반

반복기 블록을 사용하여 열거 가능한 인터페이스 형식을 반환하는 함수 멤버 또는 로컬 함수를 구현하는 경우 함수 멤버를 호출해도 반복기 블록에서 코드를 즉시 실행하지는 않습니다. 대신 열거 가능한 개체 가 만들어지고 반환됩니다.

열거 가능한 개체 GetEnumerator 또는 GetAsyncEnumerator 메서드는 반복기 블록에 지정된 코드를 캡슐화하는 열거자 개체를 반환하며, 열거자 개체 또는 MoveNext 메서드를 호출할 때 반복기 블록의 MoveNextAsync 코드 실행이 발생합니다. 열거 가능한 개체의 특징은 다음과 같습니다.

  • IEnumerableIEnumerable<T> 또는 IAsyncEnumerable<T>를 구현하며, 여기서 T는 반복기의 수율 유형입니다.
  • 함수 멤버에 전달된 인수 값(있는 경우) 및 인스턴스 값의 복사본을 사용하여 초기화됩니다.

열거 가능한 개체는 일반적으로 반복기 블록의 코드를 캡슐화하고 열거 가능한 인터페이스를 구현하는 컴파일러 생성 열거 가능 클래스의 인스턴스이지만 다른 구현 방법이 가능합니다. 컴파일러에서 열거 가능한 클래스를 생성하는 경우 해당 클래스는 함수 멤버를 포함하는 클래스에서 직접 또는 간접적으로 중첩되고 프라이빗 접근성을 가지며 컴파일러 사용을 위해 예약된 이름(§6.4.3)을 갖습니다.

열거 가능한 개체는 위에서 지정한 인터페이스보다 더 많은 인터페이스를 구현할 수 있습니다.

참고: 예를 들어, 열거 가능한 개체가 IEnumeratorIEnumerator<T>을 구현하여 열거 가능 개체이자 열거자로 사용할 수 있습니다. 일반적으로 이러한 구현은 첫 번째 호출에서 할당을 저장하기 위해 자체 인스턴스를 반환합니다 GetEnumerator. 이후 호출은 GetEnumerator새 클래스 인스턴스(일반적으로 동일한 클래스)를 반환하므로 다른 열거자 인스턴스에 대한 호출은 서로 영향을 주지 않습니다. 이전 열거자가 이미 시퀀스의 끝을 지나 열거한 경우에도 동일한 인스턴스를 반환할 수 없습니다. 소모된 열거자에 대한 모든 이후 호출은 반드시 예외를 던져야 하기 때문입니다. 끝 메모

15.15.6.2 GetEnumerator 또는 GetAsyncEnumerator 메서드

열거 가능한 개체는 GetEnumeratorIEnumerable 인터페이스의 IEnumerable<T> 메서드를 구현합니다. 두 GetEnumerator 메서드는 사용 가능한 열거자 개체를 획득하고 반환하는 공통 구현을 공유합니다. 열거자 개체는 열거 가능한 개체가 초기화될 때 저장된 인수 값과 인스턴스 값으로 초기화되지만 그렇지 않으면 §15.15.5에 설명된 대로 열거자 개체가 작동합니다.

비동기 열거 가능 개체는 GetAsyncEnumerator 인터페이스의 IAsyncEnumerable<T> 메서드의 구현을 제공합니다. 이 메서드는 사용 가능한 비동기 열거자 개체를 반환합니다. 열거자 개체는 열거 가능한 개체가 초기화될 때 저장된 인수 값과 인스턴스 값으로 초기화되지만 그렇지 않으면 §15.15.5에 설명된 대로 열거자 개체가 작동합니다.