مقارنة فارغة أو افتراضية للوسيطة العامة في C#

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

  •  09-06-2019
  •  | 
  •  

سؤال

لدي طريقة عامة محددة مثل هذا:

public void MyMethod<T>(T myArgument)

أول شيء أريد القيام به هو التحقق مما إذا كانت قيمة myArgument هي القيمة الافتراضية لهذا النوع، شيء من هذا القبيل:

if (myArgument == default(T))

لكن هذا لا يتم تجميعه لأنني لم أضمن أن T سينفذ عامل التشغيل ==.لذلك قمت بتحويل الكود إلى هذا:

if (myArgument.Equals(default(T)))

الآن يتم تجميع هذا، ولكنه سيفشل إذا كانت myArgument فارغة، وهو جزء مما أقوم باختباره.يمكنني إضافة فحص فارغ صريح مثل هذا:

if (myArgument == null || myArgument.Equals(default(T)))

الآن يبدو هذا زائدا عن الحاجة بالنسبة لي.يقترح ReSharper أيضًا تغيير الجزء myArgument == null إلى myArgument == default(T) وهو المكان الذي بدأت فيه.هل هناك طريقة أفضل لحل هذه المشكلة؟

أحتاج إلى الدعم كلاهما أنواع المراجع وأنواع القيمة.

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

المحلول

لتجنب الملاكمة، فإن أفضل طريقة لمقارنة الأدوية العامة من أجل المساواة هي مع EqualityComparer<T>.Default.هذا يحترم IEquatable<T> (بدون ملاكمة) كذلك object.Equals, ، ويتعامل مع كافة Nullable<T> الفروق الدقيقة "المرفوعة".لذلك:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

هذا سوف يتطابق مع:

  • فارغة للفصول الدراسية
  • فارغة (فارغة) ل Nullable<T>
  • صفر/خطأ/إلخ للبنيات الأخرى

نصائح أخرى

وماذا عن هذا:

if (object.Equals(myArgument, default(T)))
{
    //...
}

باستخدام static object.Equals() الطريقة تتجنب الحاجة إلى القيام بذلك null تحقق من نفسك.تأهيل المكالمة بشكل صريح مع object. ربما ليس من الضروري اعتمادًا على السياق الخاص بك، لكنني عادةً ما أبدأ بالبادئة static يستدعي اسم النوع فقط لجعل الكود أكثر قابلية للذوبان.

تمكنت من تحديد موقع أ مقالة مايكروسوفت كونكت الذي يناقش هذه المسألة بشيء من التفصيل:

لسوء الحظ، هذا السلوك حسب التصميم ولا يوجد حل سهل لتمكين الاستخدام مع معلمات النوع التي قد تحتوي على أنواع قيم.

إذا كانت الأنواع معروفة بأنها أنواع مرجعية، فإن التحميل الزائد الافتراضي المحدد على الكائن يختبر المتغيرات من أجل المساواة المرجعية، على الرغم من أن النوع قد يحدد التحميل الزائد المخصص الخاص به.يحدد المترجم التحميل الزائد الذي سيتم استخدامه بناءً على النوع الثابت للمتغير (التحديد ليس متعدد الأشكال).لذلك، إذا قمت بتغيير المثال الخاص بك لتقييد معلمة النوع العامة T بنوع مرجعي غير مختوم (مثل الاستثناء)، فيمكن للمترجم تحديد التحميل الزائد المحدد للاستخدام وسيتم تجميع التعليمات البرمجية التالية:

public class Test<T> where T : Exception

إذا كانت الأنواع معروفة بأنها أنواع قيمة، فسيتم إجراء اختبارات مساواة قيمة محددة استنادًا إلى الأنواع الدقيقة المستخدمة.لا توجد مقارنة "افتراضية" جيدة هنا نظرًا لأن المقارنات المرجعية ليست ذات معنى في أنواع القيم ولا يستطيع المترجم معرفة مقارنة القيمة المحددة التي سيتم إصدارها.يمكن للمترجم إرسال استدعاء إلى ValueType.Equals(Object) ولكن هذه الطريقة تستخدم الانعكاس وهي غير فعالة تمامًا مقارنة بمقارنات القيمة المحددة.لذلك، حتى لو كنت تريد تحديد قيد نوع القيمة على T، فلا يوجد شيء معقول للمترجم لتوليده هنا:

public class Test<T> where T : struct

في الحالة التي قدمتها، حيث لا يعرف المترجم حتى ما إذا كانت T هي قيمة أو نوع مرجعي، بالمثل لا يوجد شيء يمكن إنشاؤه يكون صالحًا لجميع الأنواع الممكنة.لن تكون المقارنة المرجعية صالحة لأنواع القيم، وقد يكون نوع ما من مقارنة القيم غير متوقع لأنواع المراجع التي لا يتم تحميلها بشكل زائد.

هنا هو ما يمكنك القيام به...

لقد تحققت من أن كلتا الطريقتين تعملان لإجراء مقارنة عامة بين أنواع المراجع والقيم:

object.Equals(param, default(T))

أو

EqualityComparer<T>.Default.Equals(param, default(T))

لإجراء مقارنات مع عامل التشغيل "=="، ستحتاج إلى استخدام إحدى الطرق التالية:

إذا كانت جميع حالات T مشتقة من فئة أساسية معروفة، فيمكنك السماح للمترجم بمعرفة ذلك باستخدام قيود النوع العامة.

public void MyMethod<T>(T myArgument) where T : MyBase

ثم يتعرف المترجم على كيفية تنفيذ العمليات MyBase ولن يلقي الخطأ "لا يمكن تطبيق عامل التشغيل '==' على المعاملات من النوع 'T' و'T'" الذي تراه الآن.

هناك خيار آخر يتمثل في تقييد T على أي نوع يتم تنفيذه IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

ومن ثم استخدم CompareTo الطريقة التي يحددها واجهة قابلة للمقارنة.

جرب هذا:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

التي ينبغي تجميعها، وتفعل ما تريد.

(محرر)

يمتلك مارك جرافيل أفضل إجابة، ولكني أردت نشر مقتطف بسيط من التعليمات البرمجية التي عملت عليها لتوضيح ذلك.ما عليك سوى تشغيل هذا في تطبيق وحدة التحكم C# البسيط:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

شيء اخر:هل يمكن لشخص لديه VS2008 تجربة ذلك كطريقة تمديد؟أنا عالق مع عام 2005 هنا ولدي فضول لمعرفة ما إذا كان ذلك مسموحًا به.


يحرر: إليك كيفية تشغيلها كطريقة تمديد:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}

للتعامل مع جميع أنواع T، بما في ذلك النوع البدائي، ستحتاج إلى التجميع في كلتا طريقتي المقارنة:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }

ستكون هناك مشكلة هنا -

إذا كنت ستسمح لهذا بالعمل مع أي نوع، فسيكون الإعداد الافتراضي (T) دائمًا خاليًا للأنواع المرجعية، و0 (أو البنية الكاملة لـ 0) لأنواع القيم.

ربما لا يكون هذا هو السلوك الذي تبحث عنه.إذا كنت تريد أن يعمل هذا بطريقة عامة، فربما تحتاج إلى استخدام الانعكاس للتحقق من نوع T، والتعامل مع أنواع القيم المختلفة عن الأنواع المرجعية.

وبدلاً من ذلك، يمكنك وضع قيد واجهة على هذا، ويمكن أن توفر الواجهة طريقة للتحقق من الإعداد الافتراضي للفئة/البنية.

أعتقد أنك ربما تحتاج إلى تقسيم هذا المنطق إلى جزأين والتحقق من وجوده فارغًا أولاً.

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

في طريقة IsNull، نعتمد على حقيقة أن كائنات ValueType لا يمكن أن تكون فارغة حسب التعريف، لذلك إذا كانت القيمة فئة مشتقة من ValueType، فإننا نعلم بالفعل أنها ليست فارغة.من ناحية أخرى، إذا لم يكن نوع القيمة، فيمكننا فقط مقارنة القيمة المصبوبة بكائن مقابل القيمة الخالية.يمكننا تجنب التحقق من ValueType من خلال الانتقال مباشرة إلى الإرسال إلى الكائن، ولكن هذا قد يعني أن نوع القيمة سيتم وضعه في مربع وهو أمر ربما نرغب في تجنبه لأنه يعني أنه تم إنشاء كائن جديد في الكومة.

في طريقة IsNullOrEmpty، نحن نتحقق من الحالة الخاصة للسلسلة.بالنسبة لجميع الأنواع الأخرى، فإننا نقوم بمقارنة القيمة (والتي نعرفها بالفعل هي لا null) مقابل قيمتها الافتراضية التي تكون خالية بالنسبة لجميع أنواع المراجع، وبالنسبة لأنواع القيم عادةً ما تكون شكلاً من أشكال الصفر (إذا كانت متكاملة).

باستخدام هذه الطرق، يتصرف التعليمة البرمجية التالية كما قد تتوقع:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}

أنا أستعمل:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}

لا أعرف ما إذا كان هذا يعمل مع متطلباتك أم لا، ولكن يمكنك تقييد T ليكون نوعًا ينفذ واجهة مثل IComparable ثم استخدم طريقة ComparesTo() من تلك الواجهة (التي يدعمها IIRC/يتعامل مع القيم الخالية) مثل هذا :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

من المحتمل أن تكون هناك واجهات أخرى يمكنك استخدامها أيضًا مثل IEquitable، وما إلى ذلك.

@يليتيريت:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

لا يمكن تطبيق عامل التشغيل '==' على المعاملات من النوع 'T' و'T'

لا أستطيع التفكير في طريقة للقيام بذلك دون الاختبار الفارغ الصريح متبوعًا باستدعاء أسلوب أو كائن Equals.Equals كما هو مقترح أعلاه.

يمكنك ابتكار حل باستخدام System.Comparison ولكن في الواقع سينتهي الأمر بمزيد من أسطر التعليمات البرمجية وزيادة التعقيد بشكل كبير.

أعتقد أنك كنت قريبًا.

if (myArgument.Equals(default(T)))

الآن يتم تجميع هذا، لكنه سيفشل إذا myArgument لاغية، وهو جزء مما أقوم باختباره.يمكنني إضافة فحص فارغ صريح مثل هذا:

كل ما عليك فعله هو عكس الكائن الذي يتم استدعاء العناصر المتساوية عليه للحصول على أسلوب أنيق وآمن.

default(T).Equals(myArgument);
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top