C # تحويل غير الملاكمة من العادة العامة إلى int؟

StackOverflow https://stackoverflow.com/questions/1189144

  •  19-09-2019
  •  | 
  •  

سؤال

بالنظر إلى معلمة عام Tenum التي ستكون دائما نوع المعمنة، هل هناك أي طريقة للإدلاء من Tenum إلى Int بدون ملاكمة / Unboxing؟

انظر رمز المثال هذا. هذا سوف مربع / unbox القيمة دون داع.

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

ما سبق C # هو وضع الإصدار الذي تم تجميعه إلى IL التالي (ملاحظة الملاكمة و Unboxing Opcodes):

.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 على نطاق واسع، لكنني لم أتمكن من العثور على مناقشة تتناول هذه الحالة المحددة.

هل كانت مفيدة؟

المحلول

لست متأكدا من أن هذا ممكن في C # دون استخدام Infection.emit. إذا كنت تستخدم Excrection.emit، فيمكنك تحميل قيمة ENUM على المكدس ثم تعاملها كما لو أنها كثيرة.

عليك أن تكتب الكثير من التعليمات البرمجية، لذلك كنت ترغب في التحقق مما إذا كنت ستكتسب أي أداء في القيام بذلك.

أعتقد أن 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 بت.)

تعديل

فكر آخر في هذا النهج. Infection.Emit يمكن إنشاء الطريقة أعلاه، ولكن الطريقة الوحيدة التي لديك من ربطها بها ستكون من خلال مكالمة افتراضية (أي أنها تنفذ واجهة / مجردة معروفة للترجمة / الملخص يمكنك الاتصال) أو مكالمة غير مباشرة (أي عبر الاحتجاج مندوب). أتصور أن كل من هذه السيناريوهات سيكون أبطأ من النفقات العامة للملاكمة / Unboxing على أي حال.

أيضا، لا تنس أن JIT ليست غبية وقد تعتني بذلك من أجلك. فيتعديل انظر تعليق إريك ليببيرت على السؤال الأصلي - يقول إن الهرير لا يؤدي هذا التحسين حاليا.)

كما هو الحال مع جميع المشكلات ذات الصلة الأداء: قياس وقياس وقياس!

نصائح أخرى

هذا يشبه الإجابات المنشورة هنا، ولكنه يستخدم أشجار التعبير ل EMIT IL للإدلاء بين الأنواع. 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();
        }
    }
}

يمكنك استبدال caster func مع التطبيقات الأخرى. سأقارنة أداء عدد قليل:

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

    كائن صب -> 42 مللي ثانية
    caster1 -> 102 مللي ثانية
    caster2 -> 102 مللي ثانية
    caster3 -> 90 مللي أمبير
    caster4 -> 101 مللي ثانية

  2. int ل int?

    كائن صب -> 651 مللي ثانية
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 109 مللي ثانية
    caster4 -> فشل

  3. int? ل int

    كائن صب -> 1957 مللي ثانية
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 124 مللي ثانية
    caster4 -> فشل

  4. enum ل int

    كائن صب -> 405 مللي ثانية
    caster1 -> فشل
    caster2 -> 102 مللي ثانية
    caster3 -> 78 مللي
    caster4 -> فشل

  5. int ل enum

    كائن صب -> 370 مللي ثانية
    caster1 -> فشل
    caster2 -> 93 مللي ثانية
    caster3 -> 87 مللي ثانية
    caster4 -> فشل

  6. int? ل enum

    كائن صب -> 2340 مللي ثانية
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 258 مللي ثانية
    caster4 -> فشل

  7. enum? ل int

    كائن صب -> 2776 مللي ثانية
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 131 مللي ثانية
    caster4 -> فشل


Expression.Convert يضع المصبوب المباشر من نوع المصدر إلى النوع المستهدف، بحيث يمكن أن تعمل على وضع مشتل صريحة وتضفي (ناهيك عن السقوط المرجعية). لذلك يمثل هذا طريقة للتعامل مع الصب الذي ممكن إلا عند عدم محاصر (أي بطريقة عامة إذا قمت بذلك (TTarget)(object)(TSource) ستنفجر إذا لم يكن تحويل الهوية (كما هو الحال في القسم السابق) أو التحويل المرجعي (كما هو موضح في قسم أحدث)). لذلك سوف أدرجها في الاختبارات.

يلقي غير محاصر:

  1. int ل double

    كائن صب -> فشل
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 109 مللي ثانية
    caster4 -> 118 مللي ثانية

  2. enum ل int?

    كائن صب -> فشل
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 93 مللي ثانية
    caster4 -> فشل

  3. int ل enum?

    كائن صب -> فشل
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 93 مللي ثانية
    caster4 -> فشل

  4. enum? ل int?

    كائن صب -> فشل
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 121 مللي ثانية
    caster4 -> فشل

  5. int? ل enum?

    كائن صب -> فشل
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 120 مللي ثانية
    caster4 -> فشل

للمتعة منه، اختبرت تحويلات نوع قليل من النوع المرجعي:

  1. PrintStringProperty ل string (تغيير التمثيل)

    صب كائن -> فشل (واضح تماما، لأنه لا يلقي مرة أخرى إلى النوع الأصلي)
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 315 مللي ثانية
    caster4 -> فشل

  2. string ل object (التمثيل الحفاظ على التحويل المرجعي)

    كائن صب -> 78 مللي ثانية
    caster1 -> فشل
    caster2 -> فشل
    caster3 -> 322 مللي ثانية
    caster4 -> فشل

اختبار مثل هذا:

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. إذا كان المقصين غير موجود من TSource ل 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 أو أشجار التعبير لديك طريقة من شأنها تحويل int إلى العادة دون الملاكمة أو إلغاء تثبيت. لاحظ أن TEnum هنا يجب أن يكون هناك نوع أساسي من int أو هذا سوف يرمي استثناءا يقول أنه لا يمكن ربطه.

تحرير: طريقة أخرى تعمل أيضا وقد تكون أقل قليلا للكتابة ...

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

هذا يعمل لتحويل 32 بت او اقل ينع من تينوم إلى int. ليس العكس. في .NET 3.5+، EnumEqualityComparer تم تحسينه لتشغيل هذا بشكل أساسي إلى عودة (int)value;

أنت تدفع النفقات العامة لاستخدام مندوب، لكنها بالتأكيد ستكون أفضل من الملاكمة.

... أنا حتى الآن ":)

ولكن فقط للتمديد على المنشور السابق (مايكل ب)، والتي فعلت كل العمل المثيرة للاهتمام

وحصلتني على مهتم بإجراء غلاف لحالة عامة (إذا كنت ترغب في التصديق عام إلى العادة في الواقع)

... وتحسين قليلا ... (ملاحظة: النقطة الرئيسية هي استخدام "ك" على Func <> / المندوبين بدلا من ذلك - كعادة، لأنواع القيمة لا تسمح بذلك)

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();
    }
}

... ل (int) - فقط (int) rel

أعتقد أنه يمكنك دائما استخدام System.Election.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;
    }
}

تقييد:
هذا يعمل في مونو. (السابقين. Unity3D)

مزيد من المعلومات حول Unity3D:
فئة Erike's 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);
    }
}

ثانيا، لا يمكن استخدام رمز إيريك في منصة AOT.
لذلك، الرمز الخاص بي هو أفضل حل لأحد.

إلى المعلق "Kristof":
أنا آسف لأنني لم أكتب كل التفاصيل.

إليك محلول إلى الأمام على التوالي مع قيود نوع عام 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.MemoryCopy من قبل مؤشر بسيط يلقي من اقتراح داهال.

آمل أن أكون متأخرا جدا ...

أعتقد أنه يجب عليك أن تنظر في حل مشكلتك بنهج مختلف بدلا من استخدام عودة محاولة لإنشاء فئة مع خصائص Readonly ثابتة.

إذا كنت ستستخدم هذا النهج، فسيكون لديك كائن "يشعر" مثل Enum ولكن سيكون لديك كل مرونة فئة مما يعني أنه يمكنك تجاوز أي من المشغلين.

هناك مزايا أخرى مثل صنع هذه الفئة جزئية ستمكنك من تحديد نفس العادة في أكثر من ملف / DLL واحد مما يجعل من الممكن إضافة قيم إلى DLL شائع دون إعادة ترجمةه.

لم أستطع العثور على أي سبب وجيه لعدم اتخاذ هذا النهج (ستكون هذه الفئة موجودة في كومة الكومة وليس على المكدس البطيء ولكن الأمر يستحق كل هذا العناء)

واسمحوا لي أن أعرف ما هو رأيك.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top