.NET 6 Preview 7에 추가된 숫자 형식에 대한 제네릭 연산 지원
(2022-07-15 업데이트) 이 기능은 C# 11에 포함될 예정이고, .NET 7 환경을 필요로 합니다.
.NET 6에 다음과 같은 기능이 추가되었다고 합니다.
Preview Features in .NET 6 – Generic Math
; https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/
이 기능에 대한 요청은 오래전부터 있었다는데, 아마도 근래의 딥러닝으로 인한 수학 라이브러리가 점점 더 중요성이 대두되면서 더 이상 버틸 수 없어 내놓게 된 것이 아닌가... 예상이 되는데요. ^^
이에 대한 의미를 느끼기 위해 간단하게 예를 하나 들어볼까요? ^^
가령, 여러분들이 다음과 같이 List를 생성했을 때, 이에 대한 합계를 구하는 코드를 제네릭 메서드로 만들고 싶다고 가정해 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>();
list.AddRange(new[] { 1, 2, 3 });
Console.WriteLine(list.합계());
}
}
public static class MyLinqExtension
{
public static T 합계<T>(this IEnumerable<T> arg)
{
T sum = default;
foreach (T item in arg)
{
sum = sum + item; // 컴파일 오류
}
return sum;
}
}
아쉽게도 위의 코드는 "sum = sum + item" 코드에서 다음과 같은 컴파일 오류가 발생합니다.
error CS0019: Operator '+' cannot be applied to operands of type 'T' and 'T'
왜냐하면, 해당 T 타입은 더하기 연산을 지원하는 타입이 아닐 수도 있기 때문입니다. 즉, .NET 5 이전까지는 T 인스턴스가 숫자형 타입이라고 강제할 수 있는 어떠한 방법도 제공되지 않았습니다.
아니, 그렇다면 기존의 IEnumerable<T>에 있던 Sum은 뭡니까?
List<int> list = new List<int>();
list.AddRange(new[] { 1, 2, 3 });
Console.WriteLine(list.Sum());
사실 위의 코드 이면에는 마이크로소프트의 눈물겨운 노력이 있었습니다. 즉, 위의 코드를 가능하게 하도록 각각의 숫자형 타입마다 동일한 이름의 확장 메서드를 제공하는 방식으로 구현을 했었습니다.
public static class Enumerable
{
public static int Sum(this IEnumerable<int> source);
public static int? Sum(this IEnumerable<int?> source);
public static long Sum(this IEnumerable<long> source);
public static long? Sum(this IEnumerable<long?> source);
public static float Sum(this IEnumerable<float> source);
public static float? Sum(this IEnumerable<float?> source);
public static double Sum(this IEnumerable<double> source);
public static double? Sum(this IEnumerable<double?> source);
public static decimal Sum(this IEnumerable<decimal> source);
public static decimal? Sum(this IEnumerable<decimal?> source);
}
심지어 selector를 인자로 받는 또 다른 Sum 메서드까지 합치면,
public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
public static int? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int?> selector)
public static long Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long> selector)
public static long? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long?> selector)
public static float Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float> selector)
public static float? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float?> selector)
public static double Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double> selector)
public static double? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double?> selector)
public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector)
public static decimal? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector)
기능 하나당 10개의 메서드를 지겹게 구현했던 것입니다. 바로 이러한 중복 문제를, .NET 6에 새롭게 추가된 인터페이스들로 인해 해결할 수 있습니다.
Operator Interface Name |
Summary |
IParseable |
Parse(string, IFormatProvider) |
ISpanParseable |
Parse(ReadOnlySpan<char>, IFormatProvider) |
|
|
IAdditionOperators |
x + y |
IBitwiseOperators |
x & y , x | y , x ^ y , and ~x |
IComparisonOperators |
x < y , x > y , x <= y , and x >= y |
IDecrementOperators |
--x and x-- |
IDivisionOperators |
x / y |
IEqualityOperators |
x == y and x != y |
IIncrementOperators |
++x and x++ |
IModulusOperators |
x % y |
IMultiplyOperators |
x * y |
IShiftOperators |
x << y and x >> y |
ISubtractionOperators |
x - y |
IUnaryNegationOperators |
-x |
IUnaryPlusOperators |
+x |
|
|
IAdditiveIdentity |
(x + T.AdditiveIdentity) == x |
IMinMaxValue |
T.MinValue and T.MaxValue |
IMultiplicativeIdentity |
(x * T.MultiplicativeIdentity) == x |
|
|
IBinaryFloatingPoint |
Members common to binary floating-point types |
IBinaryInteger |
Members common to binary integer types |
IBinaryNumber |
Members common to binary number types |
IFloatingPoint |
Members common to floating-point types |
INumber |
Members common to number types |
ISignedNumber |
Members common to signed number types |
IUnsignedNumber |
Members common to unsigned number types |
결국, .NET 6에선 기존 숫자형 타입들에 대해 위에서 정의한 인터페이스를 반영했는데요, 일례로 Int32는 다음과 같이 바뀐 것을 볼 수 있습니다.
public readonly struct Int32 : IComparable, IComparable<int>, IConvertible, IEquatable<int>,
ISpanFormattable, IFormattable, IBinaryInteger<int>, IBinaryNumber<int>,
IBitwiseOperators<int, int, int>, INumber<int>,
IAdditionOperators<int, int, int>, IAdditiveIdentity<int, int>,
IComparisonOperators<int, int>, IEqualityOperators<int, int>,
IDecrementOperators<int>, IDivisionOperators<int, int, int>,
IIncrementOperators<int>, IModulusOperators<int, int, int>,
IMultiplicativeIdentity<int, int>, IMultiplyOperators<int, int, int>,
IParseable<int>, ISpanParseable<int>, ISubtractionOperators<int, int, int>,
IUnaryNegationOperators<int, int>, IUnaryPlusOperators<int, int>,
IShiftOperators<int, int>, IMinMaxValue<int>, ISignedNumber<int>
{
//...
}
따라서 이제 처음에 우리가 만들었던 "합계" 메서드도 다음과 같이 간단하게 구현할 수 있고,
public static T 합계<T>(this IEnumerable<T> arg) where T : INumber<T>
{
T sum = T.Zero;
foreach (T item in arg)
{
sum = sum + item;
}
return sum;
}
원하는 만큼 관련 기능을 이런 식으로 추가할 수 있습니다.
List<int> list = new List<int>();
list.AddRange(new[] { 1, 2 });
Console.WriteLine(list.합계());
Console.WriteLine(list.산술평균());
public static float 산술평균<T>(this IEnumerable<T> arg) where T : INumber<T>
{
return 산술평균<T, float>(arg);
}
public static TResult 산술평균<T, TResult>(this IEnumerable<T> arg) where T : INumber<T>
where TResult : INumber<TResult>
{
return TResult.Create(arg.합계()) / TResult.Create(arg.Count());
}
대충 느낌 아시겠죠? ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
현재 Visual Studio 2022 Preview 3.0에서 C# 10 Preview와
System.Runtime.Experimental 어셈블리를 참조 추가한 .NET 6 대상의 프로젝트면 위의 코드를 테스트하는 것이 가능하며, 이는 csproj의 내용에 다음의 설정 3가지를 추가하는 것과 같습니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
</ItemGroup>
</Project>
참고로, 위에서 구현한 산술평균 메서드를 IDivisionOperators 인터페이스를 이용해 다음과 같이 구현하는 것도 가능합니다.
public static TResult ArithmeticMean<T, TResult>(this IEnumerable<T> arg) where T : INumber<T>, IDivisionOperators<T, int, TResult>
where TResult : INumber<TResult>
{
return arg.합계() / arg.Count();
}
하지만, 이것의 반환 값을 float이나 double 등의 값으로 바꾸는 경우에는 호출 시 다음과 같은 오류가 발생합니다.
// 컴파일 오류
// error CS0315: The type 'int' cannot be used as type parameter 'T' in the generic type or method 'MyLinqExtension.ArithmeticMean<T>(IEnumerable<T>)'. There is no boxing conversion from 'int' to 'System.IDivisionOperators<int, int, float>'.
Console.WriteLine(list.ArithmeticMean());
public static float ArithmeticMean<T>(this IEnumerable<T> arg) where T : INumber<T>,
IDivisionOperators<T, int, float>
{
return ArithmeticMean<T, float>(arg);
}
왜냐하면, Int32 타입은 명시적으로 IDivisionOperators<int, int, int> 인터페이스를 상속받기 때문에 위와 같이 TResult의 타입을 float으로 주는 경우 형변환 오류가 발생하는 것입니다. 마이크로소프트도 아마 저 문제를 인식했기 때문에 이를 우회하기 위해 INumber<T>.Create 메서드를 제공하게 된 것이 아닌가... 추측해 봅니다. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]