자습서: C# 11 기능 살펴보기 - 인터페이스의 정적 가상 멤버

C# 11 및 .NET 7에는 인터페이스의 정적 가상 멤버가 포함되어 있습니다. 이 기능을 사용하면 오버로드된 연산자 또는 기타 정적 멤버를 포함하는 인터페이스를 정의할 수 있습니다. 정적 멤버로 인터페이스를 정의한 후에는 해당 인터페이스를 제약 조건으로 사용하여 연산자나 기타 정적 메서드를 사용하는 제네릭 형식을 만들 수 있습니다. 오버로드된 연산자가 포함된 인터페이스를 만들지 않더라도 이 기능과 언어 업데이트로 사용하도록 설정된 제네릭 수학 클래스의 이점을 활용할 수 있습니다.

이 자습서에서는 다음 작업을 수행하는 방법을 알아봅니다.

  • 정적 멤버로 인터페이스를 정의합니다.
  • 인터페이스를 사용하여 연산자가 정의된 인터페이스를 구현하는 클래스를 정의합니다.
  • 정적 인터페이스 메서드에 의존하는 제네릭 알고리즘을 만듭니다.

필수 조건

C# 11을 지원하는 .NET 7을 실행하려면 컴퓨터를 설정해야 합니다. C# 11 컴파일러는 Visual Studio 2022 버전 17.3 또는 .NET 7 SDK부터 사용할 수 있습니다.

정적 추상 인터페이스 메서드

예제를 확인해보겠습니다. 다음 메서드는 두 double 숫자의 중간점을 반환합니다.

public static double MidPoint(double left, double right) =>
    (left + right) / (2.0);

int, short, long, floatdecimal 또는 숫자를 나타내는 모든 형식 등 모든 숫자 형식에 대해 동일한 논리가 작동합니다. +/ 연산자를 사용하고 2의 값을 정의할 수 있는 방법이 필요합니다. System.Numerics.INumber<TSelf> 인터페이스를 사용하여 이전 메서드를 다음 제네릭 메서드로 작성할 수 있습니다.

public static T MidPoint<T>(T left, T right)
    where T : INumber<T> => (left + right) / T.CreateChecked(2);  // note: the addition of left and right may overflow here; it's just for demonstration purposes

INumber<TSelf> 인터페이스를 구현하는 모든 형식은 operator +operator /에 대한 정의를 포함해야 합니다. 분모는 숫자 형식에 대해 2 값을 만들기 위해 T.CreateChecked(2)에 의해 정의되며, 이는 분모가 두 매개 변수와 동일한 형식이 되도록 강제합니다. INumberBase<TSelf>.CreateChecked<TOther>(TOther)는 지정된 값에서 해당 형식의 인스턴스를 만들고 값이 표현 가능한 범위를 벗어나는 경우 OverflowException을 throw합니다. (이 구현은 leftright가 모두 충분히 큰 값인 경우 오버플로가 발생할 가능성이 있습니다. 이러한 잠재적인 문제를 피할 수 있는 대체 알고리즘이 있습니다.)

익숙한 구문을 사용하여 인터페이스에서 정적 추상 멤버를 정의합니다. 구현을 제공하지 않는 모든 정적 멤버에 staticabstract 한정자를 추가합니다. 다음 예에서는 operator ++를 재정의하는 모든 형식에 적용할 수 있는 IGetNext<T> 인터페이스를 정의합니다.

public interface IGetNext<T> where T : IGetNext<T>
{
    static abstract T operator ++(T other);
}

형식 인수 TIGetNext<T>를 구현하는 제약 조건은 연산자의 서명에 포함 형식 또는 해당 형식 인수가 포함되도록 보장합니다. 많은 연산자는 해당 매개 변수가 형식과 일치해야 하거나 포함 형식을 구현하도록 제한되는 형식 매개 변수가 되도록 강제합니다. 이 제약 조건이 없으면 IGetNext<T> 인터페이스에서 ++ 연산자를 정의할 수 없습니다.

다음 코드를 사용하면 각 증분이 문자열에 다른 문자를 추가하는 'A' 문자의 문자열을 만드는 구조를 만들 수 있습니다.

public struct RepeatSequence : IGetNext<RepeatSequence>
{
    private const char Ch = 'A';
    public string Text = new string(Ch, 1);

    public RepeatSequence() {}

    public static RepeatSequence operator ++(RepeatSequence other)
        => other with { Text = other.Text + Ch };

    public override string ToString() => Text;
}

보다 일반적으로, "이 형식의 다음 값을 생성한다"는 의미로 ++을 정의하려는 알고리즘을 빌드할 수 있습니다. 이 인터페이스를 사용하면 명확한 코드와 결과가 생성됩니다.

var str = new RepeatSequence();

for (int i = 0; i < 10; i++)
    Console.WriteLine(str++);

앞의 예에서는 다음 출력을 생성합니다.

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

이 작은 예는 이 기능의 동기를 보여 줍니다. 연산자, 상수 값 및 기타 정적 연산에 자연 구문을 사용할 수 있습니다. 오버로드된 연산자를 포함하여 정적 멤버에 의존하는 여러 형식을 만들 때 이러한 기술을 탐색할 수 있습니다. 형식의 기능과 일치하는 인터페이스를 정의한 다음 새 인터페이스에 대한 해당 형식의 지원을 선언합니다.

제네릭 수학

인터페이스에서 연산자를 포함한 정적 메서드를 허용하는 동기 부여 시나리오는 제네릭 수학 알고리즘을 지원하는 것입니다. .NET 7 기본 클래스 라이브러리에는 많은 산술 연산자에 대한 인터페이스 정의와 INumber<T> 인터페이스에서 많은 산술 연산자를 결합하는 파생 인터페이스가 포함되어 있습니다. 이러한 형식을 적용하여 T에 대해 모든 숫자 형식을 사용할 수 있는 Point<T> 레코드를 빌드해 보겠습니다. 점은 + 연산자를 사용하여 일부 XOffsetYOffset로 이동할 수 있습니다.

dotnet new 또는 Visual Studio를 사용하여 새 콘솔 애플리케이션을 만드는 것부터 시작합니다.

Translation<T>Point<T>의 공용 인터페이스는 다음 코드와 유사해야 합니다.

// Note: Not complete. This won't compile yet.
public record Translation<T>(T XOffset, T YOffset);

public record Point<T>(T X, T Y)
{
    public static Point<T> operator +(Point<T> left, Translation<T> right);
}

Translation<T>Point<T> 형식 모두에 대해 record 형식을 사용합니다. 둘 다 두 개의 값을 저장하며 정교한 동작보다는 데이터 스토리지를 나타냅니다. operator +의 구현은 다음 코드와 같습니다.

public static Point<T> operator +(Point<T> left, Translation<T> right) =>
    left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };

이전 코드를 컴파일하려면 TIAdditionOperators<TSelf, TOther, TResult> 인터페이스를 지원한다고 선언해야 합니다. 해당 인터페이스에는 operator + 정적 메서드가 포함되어 있습니다. 이는 세 가지 형식 매개 변수(왼쪽 피연산자용, 오른쪽 피연산자용, 결과용)를 선언합니다. 일부 형식은 다양한 피연산자 및 결과 형식에 대해 +을 구현합니다. 형식 인수 TIAdditionOperators<T, T, T>를 구현한다는 선언을 추가합니다.

public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>

해당 제약 조건을 추가한 후 Point<T> 클래스는 더하기 연산자로 +를 사용할 수 있습니다. Translation<T> 선언에 동일한 제약 조건을 추가합니다.

public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;

IAdditionOperators<T, T, T> 제약 조건은 클래스를 사용하는 개발자가 점 추가 제약 조건을 충족하지 않는 형식을 사용하여 Translation을 만드는 것을 방지합니다. 이 코드가 작동하도록 Translation<T>Point<T>의 형식 매개 변수에 필요한 제약 조건을 추가했습니다. Program.cs 파일의 TranslationPoint 선언 위에 다음과 같은 코드를 추가하여 테스트할 수 있습니다.

var pt = new Point<int>(3, 4);

var translate = new Translation<int>(5, 10);

var final = pt + translate;

Console.WriteLine(pt);
Console.WriteLine(translate);
Console.WriteLine(final);

이러한 형식이 적절한 산술 인터페이스를 구현한다고 선언하면 이 코드를 더 쉽게 재사용할 수 있습니다. 첫 번째 변경 내용은 Point<T, T>IAdditionOperators<Point<T>, Translation<T>, Point<T>> 인터페이스를 구현한다고 선언하는 것입니다. Point 형식은 피연산자 및 결과에 대해 다양한 형식을 사용합니다. Point 형식은 이미 해당 서명을 사용하여 operator +를 구현하므로 선언에 인터페이스만 추가하면 됩니다.

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>

마지막으로 추가를 수행할 때 해당 형식에 대한 추가 ID 값을 정의하는 속성을 갖는 것이 유용합니다. 해당 기능에 대한 새 인터페이스가 있습니다. IAdditiveIdentity<TSelf,TResult>. {0, 0}의 변환은 덧셈 항등식입니다. 결과 점은 왼쪽 피연산자와 동일합니다. IAdditiveIdentity<TSelf, TResult> 인터페이스는 ID 값을 반환하는 하나의 읽기 전용 속성인 AdditiveIdentity를 정의합니다. 이 인터페이스를 구현하려면 Translation<T>에 몇 가지 변경이 필요합니다.

using System.Numerics;

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Translation<T> AdditiveIdentity =>
        new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}

여기에는 몇 가지 변경 내용이 있으므로 하나씩 살펴보겠습니다. 먼저, Translation 형식이 IAdditiveIdentity 인터페이스를 구현한다고 선언합니다.

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>

다음에는 다음 코드에 표시된 대로 인터페이스 멤버를 구현해 볼 수 있습니다.

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: 0, YOffset: 0);

0은 형식에 따라 다르므로 앞의 코드는 컴파일되지 않습니다. 답: 0에는 IAdditiveIdentity<T>.AdditiveIdentity를 사용합니다. 이러한 변경은 이제 제약 조건에 TIAdditiveIdentity<T>를 구현한다는 것을 포함해야 함을 의미합니다. 그 결과 다음과 같은 구현이 이루어집니다.

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);

이제 Translation<T>에 해당 제약 조건을 추가했으므로 Point<T>에도 동일한 제약 조건을 추가해야 합니다.

using System.Numerics;

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Point<T> operator +(Point<T> left, Translation<T> right) =>
        left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}

이 샘플에서는 제네릭 수학용 인터페이스가 어떻게 구성되는지 살펴보았습니다. 다음 방법에 대해 알아보았습니다.

  • 메서드가 모든 숫자 형식과 함께 사용될 수 있도록 INumber<T> 인터페이스에 의존하는 메서드를 작성합니다.
  • 하나의 수학 연산만 지원하는 형식을 구현하기 위해 추가 인터페이스에 의존하는 형식을 빌드합니다. 해당 형식은 동일한 인터페이스에 대한 지원을 선언하므로 다른 방식으로 구성될 수 있습니다. 알고리즘은 수학 연산자의 가장 자연스러운 구문을 사용하여 작성되었습니다.

이러한 기능을 실험하고 피드백을 등록합니다. Visual Studio의 피드백 보내기 메뉴 항목을 사용하거나 GitHub의 roslyn 리포지토리에서 새 문제를 만들 수 있습니다. 모든 숫자 형식에서 작동하는 제네릭 알고리즘을 빌드합니다. 형식 인수가 숫자와 유사한 기능의 하위 집합만 구현할 수 있는 이러한 인터페이스를 사용하여 알고리즘을 빌드합니다. 이러한 기능을 사용하는 새로운 인터페이스를 빌드하지 않더라도 알고리즘에서 이를 사용해 실험할 수 있습니다.

참고 항목