Spanned 인기있는 BCL 유형에 대한 스팬 호환 대안을 도입하고 스팬의 공통 작업을위한 벡터 화 솔루션을 제공하는 고성능 제로 할당 .NET 라이브러리입니다.
시작하려면 먼저 스프레드 패키지를 프로젝트에 추가하십시오. 다음 명령을 실행하여이를 수행 할 수 있습니다.
dotnet add package Spanned또는이 명령을 사용하여 패키지 관리자 콘솔을 통해 설치할 수 있습니다.
Install-Package Spanned.NET은 프레임 워크 및 런타임 의존적 인 시간이 지남에 따라 수많은 최적화 경로를 축적했습니다. 결과적으로, 단일 코드베이스에서 다른 프레임 워크에 대해 이웃이 고도로 최적화 된 코드에게는 매우 도전적이되었습니다. 따라서이 라이브러리 버전 당 하나의 프레임 워크 만 지원하기 어려운 결정을 내 렸습니다. 아래 표에서 라이브러리 버전 목록과 해당 지원 프레임 워크 버전을 찾을 수 있습니다.
| .NET 표준 2.0 | .NET 표준 2.1 | .NET 8+ | |
|---|---|---|---|
.NET 표준 2.0에 대한 지원과 오늘날 .NET Standard 2.0의 광범위한 채택을 고려할 때 모든 요구에 대해 v0.0.1 사용하려는 유혹이 될 수 있지만, 그렇게하는 것이 좋습니다. 이 레거시 프레임 워크를 사용하여 수행 할 수있는 최적화는 사실상 없습니다. System.Memory Nuget 패키지를 통해 .NET Standard 2.0에서 액세스 할 수있는 우리의 사랑하는 Span 조차도 "느린 스팬"이라고합니다. 그 Span 다시 발명 된 ArraySegment 에 지나지 않기 때문에 런타임/JIT 측에서 적절한 지원이 부족하기 때문입니다. 따라서 환경에 가장 적합한 것처럼 보이는 것이 아니라 환경에 가장 적합한 패키지 버전을 선택하십시오.
세부 사항에 들어가기 전에이 라이브러리를 사용하는 동안 발생할 수있는 일반적인 가장자리 사례에 대해 논의하고 "X가 .NET의 일부가 아닌 이유는 무엇입니까?" 요컨대, 여기에서 찾을 수있는 모든 것은 사용하기 쉽고 오용하기 쉽습니다.
명백한 요점부터 시작하겠습니다. 이 라이브러리는 할당 된 모든 바이트와 매우 중요한 경로에서 실행 시간의 모든 나노초에 치아와 손톱을 싸우는 시나리오를 위해 특별히 설계되었습니다. 전체 코드베이스를위한 한 가지 크기의 모든 솔루션이 아닙니다. 모든 시나리오 에서이 라이브러리에서 유형을 무의식적으로 통합하면 응용 프로그램의 전반적인 성능이 저하 될 수 있습니다.
필요할 때까지 먼저 나노 최적화 바다로 머리를 뛰어 넘지 마십시오.
피하는 일반적인 실수는이 라이브러리가 제공 한 유형을 가치로 전달하는 것입니다 (예, 이와 같은 것이 BCL의 일부가되어서는 안되는 이유를 이해하는 데 도움이 될 것입니다) . 예를 들어, 다음 코드는 잘 보일 수 있지만 실제로는 비참합니다.
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
sb . AppendUserName ( user ) ;
// ...
public static void AppendUserName ( this ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
} 문자열 빌더에 추가하면 내부 버퍼가 확대 될 수 있습니다. 그러나, 우리는 ValueStringBuilder 값으로 통과했기 때문에 (즉, 복사) 원래 ValueStringBuilder 그것을 알지 못하고 이미 배치 된 버퍼를 계속 사용할 것입니다.
이 접근법은 테스트 중에 소독된 입력과 함께 작동하는 것처럼 보이지만, 가끔 실패하여 코드뿐만 아니라 ValueStringBuilder 사본이 이미 풀로 돌아 왔다는 버퍼를 방해하여 앱 런타임의 임의의 일부를 깨뜨릴 수 있으므로 다른 것에 의해 재사용 될 수 있습니다.
당신은 그것에 대해 똑똑해 지려고 노력하고 문제가있는 연장 방법을 다시 작성하여 다음과 같이 문제를 해결하려고 노력할 수 있습니다.
public static void AppendUserName ( this in ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
} 이제 ValueStringBuilder 참조로 전달되므로 문제가 없어야합니까? 글쎄요. in 수정자는 의미를 전달하는 것처럼 시맨틱을 보존하는 동시에 값 유형 인스턴스 전체를 복사하는 비용을 줄이기 위해 존재합니다. 이는 제공된 ValueStringBuilder 의 상태 수정이 원래 인스턴스로 전파되지 않음을 의미합니다. 그래서 우리는 여전히 우리 손에 같은 문제가 있습니다. 값 유형 인스턴스의 내부 상태를 수정할 수있는 메소드를 구현하는 유일한 올바른 방법은 실제로 참조로 전달하는 것입니다.
ValueStringBuilder sb = new ValueStringBuilder ( 16 ) ;
AppendUserName ( ref sb , user ) ;
// ...
public static void AppendUserName ( ref ValueStringBuilder sb , User user )
{
sb . Append ( user . FirstName ) ;
sb . Append ( ' ' ) ;
sb . Append ( user . LastName ) ;
}일부는 원하는만큼 화려하지는 않지만이 솔루션은 실제로 작동하는 이점이 있습니다.
이 라이브러리가 제공하는 대부분의 유형은 Dispose() 메소드를 정의하여 아래에서 볼 수 있듯이 키워드 using 과 함께 사용할 수 있습니다.
using ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( ) ; 그러나 이것이 using 키워드와 함께 사용해야 한다는 의미는 아닙니다. 위의 코드가 실제로 어떻게 낮아 졌는지 기억하는 것이 매우 중요합니다.
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
try
{
Foo ( ref sb ) ;
return sb . ToString ( ) ;
}
finally
{
sb . Dispose ( ) ;
} 보호 지역을 만들고 관리하는 것은 무료가 아닙니다. 나노 최적화에 대한 우리의 초점을 고려할 때 여기에 영향을 줄 수 있습니다. 따라서 수동으로 호출하는 것이 바람직합니다 Dispose()
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
string result = sb . ToString ( ) ;
sb . Dispose ( ) ;
return result ;또는 주어진 유형에서 호출하는 마지막 메소드에 자동으로 정리를 수행하는 과부하가 있는지 확인하십시오.
ValueStringBuilder sb = new ValueStringBuilder ( stackalloc char [ 16 ] ) ;
Foo ( ref sb ) ;
return sb . ToString ( dispose : true ) ;최신 .NET에서는 다음 패턴을 만나는 것이 일반적입니다.
또는 코드에서 동일한 개념을 표현합니다.
const int StackAllocByteLimit = 1024 ;
T [ ] ? spanSource ;
scoped Span < T > span ;
if ( sizeof ( T ) * length > StackAllocByteLimit )
{
spanSource = ArrayPool < T > . Shared . Rent ( length ) ;
span = spanSource . AsSpan ( 0 , length ) ;
}
else
{
spanSource = null ;
span = stackalloc T [ length ] ;
}
DoSomeWorkWithSpan ( span ) ;
if ( spanSource is not null )
{
ArrayPool < T > . Shared . Return ( spanSource ) ;
} 가장 예쁘게 보일러 플레이트가 아닙니다. 실제 논리는 종종 그것에 의해 묻히게되며, 이는 이상적이지 않습니다. 이것은 SpanOwner 해결하려는 정확한 문제입니다. 여기에 동일한 논리가 있지만 모든 보일러 플레이트는 SpanOwner 뒤에 숨겨져 있습니다.
SpanOwner < T > owner = SpanOwner < T > . ShouldRent ( length ) ? SpanOwner < T > . Rent ( length ) : stackalloc T [ length ] ;
Span < T > span = owner . Span ;
DoSomeWorkWithSpan ( span ) ;
owner . Dispose ( ) ; 쓰기가 훨씬 쉽고 읽기가 훨씬 쉽고, 가장 중요한 것은이 접근법이 SpanOwner 완전히 인쇄 할 수 없도록 설계 되었기 때문에 정확히 동일한 성능을 제공합니다. JIT에 의해 코드에서 완전히 제거 될 수 있습니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| _spanowner_int32없이 | 5.134 ns | 0.0425 ns | 1.00 | 315 b |
| with_spanowner_int32 | 4.908 ns | 0.0168 ns | 0.96 | 310 b |
ValueStringBuilder 스택-할당 버퍼를 지원하도록 설계된 StringBuilder 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueStringBuilder 스택에 맞을 수있는 소형 줄을 구축하는 데 적합합니다. 그러나 더 큰 문자열에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 어떤 것에 사용해서는 안됩니다. ValueStringBuilder 의 진정한 광채는 문자열로 구체화 될 필요가없는 짧은 문자 시퀀스를 만들어야 할 때 나타납니다.
ValueStringBuilder StringBuilder 의 모든 기능을 반영하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품으로 완벽하게 사용될 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the builder to rent an array from `ArrayPool<char>.Shared`.
ValueStringBuilder sb = new ( stackalloc char [ 256 ] ) ;
// `ValueStringBuilder` provides a custom interpolated string handler,
// ensuring such operations do not allocate any new strings.
sb . Append ( $ "Hello, { user . Name } ! Your ID is: { user . id } " ) ;
// Unlike `StringBuilder`, `ValueStringBuilder` can be represented
// as a readonly span. Thus, you don't need to actually materialize
// the string you've built in lots of cases.
DisplayWelcome ( ( ReadOnlySpan < char > ) sb ) ;
// Remember to dispose of the builder to return
// a rented buffer, if any, back to the pool.
sb . Dispose ( ) ; ValueList<T> 스택-할당 버퍼를 지원하도록 설계된 List<T> 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueList<T> 스택에 맞을 수있는 소량의 데이터를 처리하는 데 적합합니다. 그러나 더 큰 데이터 세트에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 것에 사용해서는 안됩니다.
ValueList<T> List<T> 의 모든 기능을 반영하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품 역할을 할 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the list to rent an array from `ArrayPool<T>.Shared`.
ValueList < int > list = new ( stackalloc int [ 10 ] ) ;
list . Add ( 0 ) ;
list . Add ( 1 ) ;
list . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) list ) ;
// Remember to dispose of the list to return
// a rented buffer, if any, back to the pool.
list . Dispose ( ) ; ValueStack<T> 스택-할당 버퍼를 지원하도록 설계된 Stack<T> 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueStack<T> 스택에 맞을 수있는 소량의 데이터를 처리하는 데 적합합니다. 그러나 더 큰 데이터 세트에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 것에 사용해서는 안됩니다.
ValueStack<T> Stack<T> 의 모든 기능을 미러링하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품 역할을 할 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the stack to rent an array from `ArrayPool<T>.Shared`.
ValueStack < int > stack = new ( stackalloc int [ 10 ] ) ;
stack . Push ( 0 ) ;
stack . Push ( 1 ) ;
stack . Push ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) stack ) ;
// Remember to dispose of the stack to return
// a rented buffer, if any, back to the pool.
stack . Dispose ( ) ; ValueQueue<T> 스택-할당 버퍼를 지원하도록 설계된 Queue<T> 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueQueue<T> 스택에 맞을 수있는 소량의 데이터를 처리하는 데 적합합니다. 그러나 더 큰 데이터 세트에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 것에 사용해서는 안됩니다.
ValueQueue<T> Queue<T> 의 모든 기능을 반영하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품 역할을 할 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the queue to rent an array from `ArrayPool<T>.Shared`.
ValueQueue < int > queue = new ( stackalloc int [ 10 ] ) ;
queue . Enqueue ( 0 ) ;
queue . Enqueue ( 1 ) ;
queue . Enqueue ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) queue ) ;
// Remember to dispose of the queue to return
// a rented buffer, if any, back to the pool.
queue . Dispose ( ) ; ValueSet<T> 스택-할당 버퍼를 지원하도록 설계된 HashSet<T> 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueSet<T> 는 스택에 맞을 수있는 소량의 데이터를 처리하는 데 적합합니다. 그러나 더 큰 데이터 세트에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 것에 사용해서는 안됩니다.
ValueSet<T> HashSet<T> 의 모든 기능을 반영하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품 역할을 할 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the set to rent an array from `ArrayPool<T>.Shared`.
ValueSet < int > set = new ( stackalloc int [ 10 ] ) ;
set . Add ( 0 ) ;
set . Add ( 1 ) ;
set . Add ( 2 ) ;
DoSomethingWithIntegers ( ( ReadOnlySpan < int > ) set ) ;
// Remember to dispose of the set to return
// a rented buffer, if any, back to the pool.
set . Dispose ( ) ; ValueDictionary<TKey, TValue> 스택-할당 버퍼를 지원하도록 설계된 Dictionary<TKey, TValue> 의 재 구현입니다. 공유 어레이 풀을 사용하여 필요할 때 내부 버퍼를 확장 할 수 있습니다. ValueDictionary<TKey, TValue> 스택에 맞을 수있는 소량의 데이터를 처리하는 데 적합합니다. 그러나 더 큰 데이터 세트에서의 작업은 앱의 전반적인 성능을 저하시키기 때문에 다른 것에 사용해서는 안됩니다.
ValueDictionary<TKey, TValue> Dictionary<TKey, TValue> 의 모든 기능을 반영하고 동일한 단위 테스트 세트를 성공적으로 전달하여 대부분의 시나리오에서 드롭 인 교체품으로 완벽하게 사용될 수 있습니다.
// Note that providing a capacity instead of a buffer will force
// the dictionary to rent an array from `ArrayPool<T>.Shared`.
ValueDictionary < int , string > dictionary = new ( 10 ) ;
dictionary . Add ( 0 , "zero" ) ;
dictionary . Add ( 1 , "one" ) ;
dictionary . Add ( 2 , "two" ) ;
DoSomethingWithPairs ( ( ReadOnlySpan < KeyValuePair < int , string > > ) dictionary ) ;
// Remember to dispose of the dictionary to return
// a rented buffer, if any, back to the pool.
dictionary . Dispose ( ) ; .Min() 및 .Max() 는 스팬에서 최소/최대 값을 찾는 데 도움이되는 확장 메소드입니다. Enumerable.Min() 및 Enumerable.Max() 와 달리 모든 지원되는 유형에 대해 벡터화되며 플로팅 포인트 수에 대한 최적화를 제공하지 않습니다.
그러나 부동 소수점 숫자 (예 : float 및 double )에는 약간의 문제가 있으며이 문제의 이름은 NaN 입니다. 아시다시피, NaN 숫자보다 크거나 작지 않으며, 그 자체로도 숫자와 같지 않습니다. 따라서, NaN 이 제공된 시퀀스에 존재하는 경우, 정기적 인 비교 작업의 결과에만 의존하는 순진한 구현을 방해 할 수 있습니다. 따라서, NaN 이 제공된 시퀀스에 존재하는 경우, 정기적 인 비교 작업의 결과에만 의존하는 순진한 구현을 방해 할 수 있습니다. 따라서,이 부동 소수점 비교를 설명하지 않는 것은 옵션이 아닙니다.
Spanned 모든 NaN 관련 검사를 고도로 효율적인 방식으로 사용하여 최적화되지 않은 솔루션에 비해 상당한 성능 향상을 제공합니다. 그러나 NaN 을 설명 할 필요가 없다면 성능이 더 나을 수 있습니다. 그렇기 때문에 .UnsafeMin() 및 .UnsafeMax() 존재하는 이유입니다. 이 방법은 부동 소수점 수를 포함하는 스팬에만 적합하며 NaN 의 존재를 인정하지 않고 비교 작업을 수행하여 모든 관련 점검을 제거합니다. 따라서 부동 소수점 숫자가 소독되고 NaN S를 포함 할 수 없다고 확신하면 .Min() 및 .Max() 작업에서 더 많은 성능을 짜낼 수 있습니다.
.Min() 과 .UnsafeMin() 의 차이는 눈에 띄지 않을 수 있습니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| min_loop_single | 3,919.5 ns | 15.75 ns | 1.00 | 207 b |
| min_linq_single | 4,030.3 ns | 37.38 ns | 1.03 | 570 b |
| min_span_single | 611.1 ns | 8.55 ns | 0.16 | 534 b |
| unsafemin_span_single | 569.0 ns | 1.82 ns | 0.15 | 319 b |
성능 간격은 .Max() 와 .UnsafeMax() 사이에 상당히 상당 해집니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| max_loop_single | 3,849.2 ns | 36.97 ns | 1.00 | 215 b |
| max_linq_single | 3,936.4 ns | 53.51 ns | 1.02 | 643 b |
| max_span_single | 901.7 ns | 7.12 ns | 0.23 | 606 b |
| unsafemax_span_single | 551.8 ns | 3.06 ns | 0.14 | 321 b |
.Sum() 은 스팬에서 모든 값의 합을 계산하는 데 도움이되는 확장 방법입니다. Enumerable.Sum() 과 달리 모든 지원 유형에 대해 벡터화되어 벡터화가 부족할뿐만 아니라 대부분의 숫자 유형에 대해 과부하를 전혀 제공하지 않습니다.
.Min() 및 .Max() 와 유사하게 .Sum() .UnsafeSum() 이라는 이름의 사악한 트윈을 가지고 있습니다. 합산 계산으로 인해 정수 오버플로/언더 플로가 발생하면 기본 방법이 OverflowException 던집니다. 물론 오버플로 경비원은 비용이 많이 든다. 그리고 그것은 무시할만한 것이 아닙니다. 따라서 입력이 소독되고 오버플로를 유발할 수 없거나 정수 오버플로가 작업 컨텍스트에서 예상되는 동작 인 경우 .UnsafeSum() 자유롭게 사용하십시오. .Sum() 의 두 배, 루프 내에서 합계를 계산하는 것보다 34 배 빠르며 LINQ를 통해 합계를 계산하는 것보다 130 배 빠르게 캐주얼합니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| sum_loop_int16 | 3,820.0 ns | 7.04 ns | 1.00 | 128 b |
| sum_linq_int16 | 14,472.6 ns | 281.83 ns | 3.80 | 732 b |
| sum_span_int16 | 214.6 ns | 2.43 ns | 0.06 | 413 b |
| unsafesum_span_int16 | 111.8 ns | 1.00 ns | 0.03 | 200 b |
.LongSum() 은 64 비트 어큐뮬레이터 (즉, 서명되지 않은 정수의 경우 long , 부호없는 정수의 경우 ulong , float 에 대한 double 배)를 사용하여 스팬에서 모든 값의 합계를 계산하는 데 도움이되는 확장 방법입니다. 원래 유형의 최대/최소값보다 큰 결과를 저장할 수 있습니다 (EG, int.MaxValue + int.MaxValue 의 수학적 정확한 결과를 저장할 수 없습니다. int ) . 모든 지원 유형에 대해 벡터화되며 LINQ에는 적절한 대안이 없습니다 (따라서 아래 벤치 마크는 약간 불공평합니다) .
.LongSum() "안전하지 않은"상대가 없습니다. int 의 int.MaxValue 요소를 저장하는 가장 큰 범위조차도 64 비트 축합기 ( (long)int.MaxValue * (long)int.MaxValue < long.MaxValue )의 오버플로를 유발할 수 없기 때문입니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| longsum_loop_int16 | 2,537.1 ns | 21.30 ns | 1.00 | 98 b |
| longsum_linq_int16 | 14,372.0 ns | 130.00 ns | 5.67 | 734 b |
| longsum_span_int16 | 251.0 ns | 2.38 ns | 0.10 | 390 b |
.Average() 는 스팬에서 모든 값의 평균을 계산하는 데 도움이되는 확장 방법입니다. Enumerable.Average() 와 달리 모든 지원되는 유형에 대해 벡터화되며, 이는 32 비트 서명 정수 (즉, int s)에 대한 어느 정도의 최적화 만 제공합니다.
후드 아래에서 .Average() .LongSum() 사용하여 정수 오버플로를 피하면서 모든 요소의 합을 계산합니다. 그러나 입력이 소독되어 있고 하나를 유발할 수없는 경우 .UnsafeAverage() 사용하는 .UnsafeSum() 로 전환 할 수 있으며 오버플로 가드에 귀중한 실행 시간을 소비하지 않습니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| 평균 _loop_int16 | 2,482.1 ns | 20.04 ns | 1.00 | 241 b |
| 평균 _linq_int16 | 13,198.2 ns | 97.67 ns | 5.31 | 1,016 b |
| 평균 _span_int16 | 257.8 ns | 3.61 ns | 0.10 | 593 b |
| unsafeaverage_span_int16 | 116.7 ns | 1.27 ns | 0.05 | 128 b |
.FillSequential() 은 순차적 인 숫자 값으로 주어진 범위를 채우는 데 도움이되는 확장 방법입니다. 모든 지원 유형에 대해 벡터화되며 LINQ에는 대안이 없습니다.
| 방법 | 평균 | stddev | 비율 | 코드 크기 |
|---|---|---|---|---|
| CLONDECTEMATION_LOOP_INT16 | 2,499.4 ns | 28.47 ns | 1.00 | 118 b |
| CLONDECTENDIAL_SPAN_INT16 | 169.2 ns | 0.18 ns | 0.07 | 660 b |
.IndexOf() , .LastIndexOf() 및 .Contains() MemoryExtensions 에 의해 제공되기 때문에 친숙해 보일 수 있습니다. 그러나 두 가지 문제가 있습니다.
IEquatable<T> 구현하려면 구현되지 않은 일반 컨텍스트에서 접근 할 수 없습니다.IEqualityComparer<T> 구현을 지원하지 않습니다.이 라이브러리에서 제공하는 이러한 방법의 구현은이 두 가지 문제를 모두 해결합니다.
MIT 라이센스의 조건에 따라 라이센스.