자습서: 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
, float
decimal
또는 숫자를 나타내는 모든 형식 등 모든 숫자 형식에 대해 동일한 논리가 작동합니다. +
및 /
연산자를 사용하고 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합니다. (이 구현은 left
와 right
가 모두 충분히 큰 값인 경우 오버플로가 발생할 가능성이 있습니다. 이러한 잠재적인 문제를 피할 수 있는 대체 알고리즘이 있습니다.)
익숙한 구문을 사용하여 인터페이스에서 정적 추상 멤버를 정의합니다. 구현을 제공하지 않는 모든 정적 멤버에 static
및 abstract
한정자를 추가합니다. 다음 예에서는 operator ++
를 재정의하는 모든 형식에 적용할 수 있는 IGetNext<T>
인터페이스를 정의합니다.
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
형식 인수 T
가 IGetNext<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>
레코드를 빌드해 보겠습니다. 점은 +
연산자를 사용하여 일부 XOffset
및 YOffset
로 이동할 수 있습니다.
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 };
이전 코드를 컴파일하려면 T
가 IAdditionOperators<TSelf, TOther, TResult>
인터페이스를 지원한다고 선언해야 합니다. 해당 인터페이스에는 operator +
정적 메서드가 포함되어 있습니다. 이는 세 가지 형식 매개 변수(왼쪽 피연산자용, 오른쪽 피연산자용, 결과용)를 선언합니다. 일부 형식은 다양한 피연산자 및 결과 형식에 대해 +
을 구현합니다. 형식 인수 T
가 IAdditionOperators<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 파일의 Translation
및 Point
선언 위에 다음과 같은 코드를 추가하여 테스트할 수 있습니다.
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
를 사용합니다. 이러한 변경은 이제 제약 조건에 T
가 IAdditiveIdentity<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 리포지토리에서 새 문제를 만들 수 있습니다. 모든 숫자 형식에서 작동하는 제네릭 알고리즘을 빌드합니다. 형식 인수가 숫자와 유사한 기능의 하위 집합만 구현할 수 있는 이러한 인터페이스를 사용하여 알고리즘을 빌드합니다. 이러한 기능을 사용하는 새로운 인터페이스를 빌드하지 않더라도 알고리즘에서 이를 사용해 실험할 수 있습니다.
참고 항목
.NET