Nice programing

일반 열거 형을 int로 C # 비 박싱 변환?

nicepro 2020. 12. 4. 20:38
반응형

일반 열거 형을 int로 C # 비 박싱 변환?


항상 열거 형 유형이 될 일반 매개 변수 TEnum이 주어지면 boxing / unboxing없이 TEnum에서 int로 캐스트 할 수있는 방법이 있습니까?

이 예제 코드를 참조하십시오. 불필요하게 값을 박스 / 박스 해제합니다.

private int Foo<TEnum>(TEnum value)
    where TEnum : struct  // C# does not allow enum constraint
{
    return (int) (ValueType) value;
}

위의 C #은 다음 IL로 컴파일 된 릴리스 모드입니다 (boxing 및 unboxing opcode 참고).

.method public hidebysig instance int32  Foo<valuetype 
    .ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  box        !!TEnum
  IL_0006:  unbox.any  [mscorlib]System.Int32
  IL_000b:  ret
}

Enum 변환은 SO에서 광범위하게 처리되었지만이 특정 사례를 다루는 토론을 찾을 수 없습니다.


Reflection.Emit을 사용하지 않고 C #에서 이것이 가능한지 잘 모르겠습니다. Reflection.Emit을 사용하는 경우 열거 형 값을 스택에로드 한 다음 int 인 것처럼 처리 할 수 ​​있습니다.

하지만 꽤 많은 코드를 작성해야하므로이 작업에서 실제로 성능을 얻을 수 있는지 확인하고 싶을 것입니다.

동등한 IL은 다음과 같습니다.

.method public hidebysig instance int32  Foo<valuetype 
    .ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
  .maxstack  8
  IL_0000:  ldarg.1
  IL_000b:  ret
}

열거 형이 long(64 비트 정수) 에서 파생 된 경우 실패합니다 .

편집하다

이 접근 방식에 대한 또 다른 생각. Reflection.Emit은 위의 메서드를 만들 수 있지만 바인딩 할 수있는 유일한 방법은 가상 호출 (즉, 호출 할 수있는 컴파일 타임 알려진 인터페이스 / 추상을 구현) 또는 간접 호출 (즉, 위임 호출을 통해). 이 두 시나리오 모두 권투 / 언 박싱 오버 헤드보다 느릴 것이라고 생각합니다.

또한 JIT가 멍청하지 않고이를 처리 할 수 ​​있음을 잊지 마십시오. ( 편집 은 원래 질문에 대한 Eric Lippert의 의견을 참조하십시오-그는 지터가 현재이 최적화를 수행하지 않는다고 말합니다. )

모든 성능 관련 문제와 마찬가지로 측정, 측정, 측정!


이것은 여기에 게시 된 답변과 유사하지만 표현식 트리를 사용하여 유형 간 캐스트를 방출합니다. Expression.Convert트릭을 수행합니다. 컴파일 된 대리자 (캐스터)는 내부 정적 클래스에 의해 캐시됩니다. 인자로부터 소스 객체를 추론 할 수 있기 때문에 더 깨끗한 호출을 제공한다고 생각합니다. 예를 들어 일반적인 컨텍스트 :

static int Generic<T>(T t)
{
    int variable = -1;

    // may be a type check - if(...
    variable = CastTo<int>.From(t);

    return variable;
}

클래스:

/// <summary>
/// Class to cast to type <see cref="T"/>
/// </summary>
/// <typeparam name="T">Target type</typeparam>
public static class CastTo<T>
{
    /// <summary>
    /// Casts <see cref="S"/> to <see cref="T"/>.
    /// This does not cause boxing for value types.
    /// Useful in generic methods.
    /// </summary>
    /// <typeparam name="S">Source type to cast from. Usually a generic type.</typeparam>
    public static T From<S>(S s)
    {
        return Cache<S>.caster(s);
    }    

    private static class Cache<S>
    {
        public static readonly Func<S, T> caster = Get();

        private static Func<S, T> Get()
        {
            var p = Expression.Parameter(typeof(S));
            var c = Expression.ConvertChecked(p, typeof(T));
            return Expression.Lambda<Func<S, T>>(c, p).Compile();
        }
    }
}

casterfunc를 다른 구현으로 바꿀 수 있습니다 . 몇 가지 성능을 비교해 보겠습니다.

direct object casting, ie, (T)(object)S

caster1 = (Func<T, T>)(x => x) as Func<S, T>;

caster2 = Delegate.CreateDelegate(typeof(Func<S, T>), ((Func<T, T>)(x => x)).Method) as Func<S, T>;

caster3 = my implementation above

caster4 = EmitConverter();
static Func<S, T> EmitConverter()
{
    var method = new DynamicMethod(string.Empty, typeof(T), new[] { typeof(S) });
    var il = method.GetILGenerator();

    il.Emit(OpCodes.Ldarg_0);
    if (typeof(S) != typeof(T))
    {
        il.Emit(OpCodes.Conv_R8);
    }
    il.Emit(OpCodes.Ret);

    return (Func<S, T>)method.CreateDelegate(typeof(Func<S, T>));
}

박스형 캐스트 :

  1. int ...에 int

    오브젝트 캐스팅->
    42ms caster1->
    102ms caster2->
    102ms caster3->
    90ms caster4-> 101ms

  2. int ...에 int?

    오브젝트 캐스팅->
    651ms caster1-> fail
    caster2-> fail
    caster3->
    109ms caster4-> fail

  3. int? ...에 int

    오브젝트 캐스팅->
    1957ms caster1-> fail
    caster2-> fail
    caster3->
    124ms caster4-> fail

  4. enum ...에 int

    오브젝트 캐스팅-> 405 ms
    caster1-> fail
    caster2-> 102 ms
    caster3-> 78 ms
    caster4-> fail

  5. int ...에 enum

    오브젝트 캐스팅->
    370ms caster1-> fail
    caster2->
    93ms caster3->
    87ms caster4-> fail

  6. int? ...에 enum

    오브젝트 캐스팅-> 2340 ms
    caster1-> fail
    caster2-> fail
    caster3-> 258 ms
    caster4-> fail

  7. enum? ...에 int

    오브젝트 캐스팅-> 2776 ms
    caster1-> fail
    caster2-> fail
    caster3-> 131 ms
    caster4-> fail


Expression.Convert소스 유형에서 대상 유형으로 직접 캐스트를 배치하므로 명시 적 및 암시 적 캐스트 (참조 캐스트는 말할 것도 없음)를 해결할 수 있습니다. 따라서 이것은 박스 화되지 않은 경우에만 가능한 캐스팅을 처리하는 방법을 제공합니다 (즉, 일반 메서드에서 수행 (TTarget)(object)(TSource)하면 ID 변환 (이전 섹션에서와 같이) 또는 참조 변환 (나중 섹션에서 볼 수 있음)이 아닌 경우 폭발합니다. )). 그래서 나는 그것들을 테스트에 포함시킬 것입니다.

박스형 캐스트 ​​:

  1. int ...에 double

    오브젝트 캐스팅-> 페일
    캐스터 1-> 페일
    캐스터 2-> 페일
    캐스터 3->
    109ms 캐스터 4-> 118ms

  2. enum ...에 int?

    개체 캐스팅->
    caster1 실패 ->
    caster2 실패 ->
    caster3 실패 ->
    caster4 93ms-> 실패

  3. int ...에 enum?

    개체 캐스팅->
    caster1 실패 ->
    caster2 실패 ->
    caster3 실패 ->
    caster4 93ms-> 실패

  4. enum? ...에 int?

    객체 캐스팅->
    caster1 실패 ->
    caster2 실패 ->
    caster3 실패 ->
    121ms caster4-> 실패

  5. int? ...에 enum?

    오브젝트 캐스팅->
    캐스터 1 실패 ->
    캐스터 2 실패 ->
    캐스터 3 실패 ->
    캐스터 4 120ms-> 실패

재미를 위해 몇 가지 참조 유형 변환을 테스트했습니다 .

  1. PrintStringProperty~ string(표현 변경)

    개체 캐스팅-> 실패 (원래 유형으로 다시 캐스팅되지 않았기 때문에 매우
    분명함 ) caster1->
    caster2 실패 ->
    caster3 실패 -> 315 ms
    caster4-> 실패

  2. stringto object(참조 변환을 유지하는 표현)

    오브젝트 캐스팅-> 78 ms
    caster1-> fail
    caster2-> fail
    caster3-> 322 ms
    caster4-> fail

다음과 같이 테스트했습니다.

static void TestMethod<T>(T t)
{
    CastTo<int>.From(t); //computes delegate once and stored in a static variable

    int value = 0;
    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000000; i++) 
    {
        value = (int)(object)t; 

        // similarly value = CastTo<int>.From(t);

        // etc
    }
    watch.Stop();
    Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}

노트 :

  1. 내 추정치는 이것을 최소한 십만 번 실행하지 않으면 그만한 가치가 없으며 권투에 대해 걱정할 것이 거의 없다는 것입니다. 대리자를 캐싱하면 메모리에 타격이 있습니다. 그러나 그 한계를 넘어서 , 특히 nullables를 포함하는 캐스팅의 경우 속도 향상이 중요 합니다.

  2. 그러나 CastTo<T>클래스 의 진정한 장점은 (int)double일반 컨텍스트에서 와 같이 박스 화되지 않은 캐스트를 허용 할 때 입니다. 따라서 (int)(object)double이러한 시나리오에서는 실패합니다.

  3. Expression.ConvertChecked대신 Expression.Convert산술 오버플로와 언더 플로가 확인되도록 대신 사용 했습니다 (예 : 예외 발생). il은 런타임 중에 생성되고 확인 된 설정은 컴파일 시간이므로 호출 코드의 확인 된 컨텍스트를 알 수있는 방법이 없습니다. 이것은 당신이 스스로 결정해야 할 것입니다. 하나를 선택하거나 둘 다에 과부하를 제공하십시오 (더 좋음).

  4. 에서 TSourceto 로 캐스트가 없으면 TTarget대리자가 컴파일되는 동안 예외가 발생합니다. 기본값 가져 오기와 같은 다른 동작을 원하는 경우 TTarget대리자를 컴파일하기 전에 리플렉션을 사용하여 형식 호환성을 확인할 수 있습니다. 생성되는 코드를 완전히 제어 할 수 있습니다. 하지만 매우 까다로울 것입니다. 참조 호환성 ( IsSubClassOf, IsAssignableFrom), 변환 연산자 존재 (해키가 됨), 기본 유형 간의 일부 내장 유형 변환 가능성을 확인해야합니다. 극도로 해키가 될 것입니다. 더 쉬운 것은 예외를 포착하고 ConstantExpression. as던지지 않는 키워드의 동작을 모방 할 수있는 가능성을 언급하는 것 뿐입니다. 그것을 멀리하고 관습에 충실하는 것이 좋습니다.


내가 파티에 너무 늦었다는 것을 알고 있지만 이와 같이 안전한 캐스트를 수행해야하는 경우 다음을 사용하여 사용할 수 있습니다 Delegate.CreateDelegate.

public static int Identity(int x){return x;}
// later on..
Func<int,int> identity = Identity;
Delegate.CreateDelegate(typeof(Func<int,TEnum>),identity.Method) as Func<int,TEnum>

이제 쓰기 Reflection.Emit또는 표현식 트리없이 boxing 또는 unboxing없이 int를 enum으로 변환하는 메서드가 있습니다. 참고 것을 TEnum여기의 기본 유형이 있어야합니다 int또는이는 바인딩 할 수 없다는 예외가 발생합니다.

편집 : 너무 작동하고 작성하는 것이 약간 적을 수있는 또 다른 방법 ...

Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;

이것은 32 비트 이하의 열거 형을 TEnum에서 int 로 변환 하는 데 사용됩니다. 그 반대는 아닙니다. .Net 3.5+에서는 EnumEqualityComparer기본적으로이를 수익으로 전환하도록 최적화되어 있습니다 (int)value.

대리자를 사용하는 데 드는 오버 헤드를 지불하고 있지만 확실히 복싱보다 낫습니다.


... 나는 '나중에'입니다 :)

그러나 모든 흥미로운 작업을 수행 한 이전 게시물 (Michael B)을 확장하기 위해

제네릭 케이스에 대한 래퍼를 만드는 데 관심이 생겼습니다 (실제로 제네릭을 enum으로 캐스트하려는 경우)

... 그리고 약간 최적화되었습니다 ... (참고 : 요점은 대신 Func <> / delegates에서 'as'를 사용하는 것입니다.-Enum으로 값 유형은 허용하지 않습니다)

public static class Identity<TEnum, T>
{
    public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}

... 다음과 같이 사용할 수 있습니다 ...

enum FamilyRelation { None, Father, Mother, Brother, Sister, };
class FamilyMember
{
    public FamilyRelation Relation { get; set; }
    public FamilyMember(FamilyRelation relation)
    {
        this.Relation = relation;
    }
}
class Program
{
    static void Main(string[] args)
    {
        FamilyMember member = Create<FamilyMember, FamilyRelation>(FamilyRelation.Sister);
    }
    static T Create<T, P>(P value)
    {
        if (typeof(T).Equals(typeof(FamilyMember)) && typeof(P).Equals(typeof(FamilyRelation)))
        {
            FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);
            return (T)(object)new FamilyMember(rel);
        }
        throw new NotImplementedException();
    }
}

... for (int)-just (int) rel


항상 System.Reflection.Emit을 사용하여 동적 메서드를 만들고 검증 할 수는 없지만 권투없이이를 수행하는 지침을 내보낼 수 있다고 생각합니다.


가장 간단하고 빠른 방법이 있습니다.
(약간 제한이 있습니다. :-))

public class BitConvert
{
    [StructLayout(LayoutKind.Explicit)]
    struct EnumUnion32<T> where T : struct {
        [FieldOffset(0)]
        public T Enum;

        [FieldOffset(0)]
        public int Int;
    }

    public static int Enum32ToInt<T>(T e) where T : struct {
        var u = default(EnumUnion32<T>);
        u.Enum = e;
        return u.Int;
    }

    public static T IntToEnum32<T>(int value) where T : struct {
        var u = default(EnumUnion32<T>);
        u.Int = value;
        return u.Enum;
    }
}

제한 사항 :
이것은 Mono에서 작동합니다. (예 : Unity3D)

Unity3D에 대한 추가 정보 :
ErikE의 CastTo 클래스는이 문제를 해결하는 정말 깔끔한 방법입니다.
그러나 Unity3D에서 그대로 사용할 수 없습니다.

먼저 아래와 같이 수정해야합니다.
(모노 컴파일러는 원래 코드를 컴파일 할 수 없기 때문에)

public class CastTo {
    protected static class Cache<TTo, TFrom> {
        public static readonly Func<TFrom, TTo> Caster = Get();

        static Func<TFrom, TTo> Get() {
            var p = Expression.Parameter(typeof(TFrom), "from");
            var c = Expression.ConvertChecked(p, typeof(TTo));
            return Expression.Lambda<Func<TFrom, TTo>>(c, p).Compile();
        }
    }
}

public class ValueCastTo<TTo> : ValueCastTo {
    public static TTo From<TFrom>(TFrom from) {
        return Cache<TTo, TFrom>.Caster(from);
    }
}

둘째, ErikE의 코드는 AOT 플랫폼에서 사용할 수 없습니다.
따라서 내 코드는 Mono를위한 최상의 솔루션입니다.

'크리스토프'의 논평자에게 :
모든 세부 사항을 쓰지 않은 것이 유감입니다.


다음은 C # 7.3의 관리되지 않는 제네릭 형식 제약 조건을 사용하는 매우 간단한 솔루션입니다.

    using System;
    public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
    {
        /// <summary>
        /// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
        /// bits that cannot be captured within <typeparam name="TResult"></typeparam>'s size will be clipped.
        /// </summary>
        public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
        {
            unsafe
            {
                if( sizeof(TResult) > sizeof(TEnum) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TResult o = default;
                    *((TEnum*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TResult*) & value;
                }
            }
        }

        /// <summary>
        /// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
        /// through pointer cast.
        /// Does not throw if the sizes don't match, clips to smallest data-type instead.
        /// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
        /// bits that cannot be captured within <typeparam name="TEnum"></typeparam>'s size will be clipped.
        /// </summary>
        public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
        {
            unsafe
            {

                if( sizeof(TEnum) > sizeof(TSource) )
                {
                    // We might be spilling in the stack by taking more bytes than value provides,
                    // alloc the largest data-type and 'cast' that instead.
                    TEnum o = default;
                    *((TSource*) & o) = value;
                    return o;
                }
                else
                {
                    return * (TEnum*) & value;
                }
            }
        }
    }

프로젝트 구성에 안전하지 않은 토글이 필요합니다.

용법:

int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );

편집 : Buffer.MemoryCopydahall의 제안에서 캐스트 된 간단한 포인터 로 대체 되었습니다.


너무 늦지 않았 으면 좋겠어요 ...

I think that you should consider to solve your problem with a different approach instead of using Enums try to creating a class with a public static readonly properties.

if you will use that approach you will have an object that "feels" like an Enum but you will have all the flexibility of a class which means that you can override any of the operators.

there are other advantages like making that class a partial which will enable you to define the same enum in more then one file/dll which makes it possible to add values to a common dll without recompiling it.

I couldn't find any good reason not to take that approach (this class will be located in the heap and not on the stack which is slower but it's worth it)

please let me know what you think.

참고URL : https://stackoverflow.com/questions/1189144/c-sharp-non-boxing-conversion-of-generic-enum-to-int

반응형