Không so sánh hoặc so sánh mặc định của đối số chung trong C #


288

Tôi có một phương thức chung được định nghĩa như thế này:

public void MyMethod<T>(T myArgument)

Điều đầu tiên tôi muốn làm là kiểm tra xem giá trị của myArgument có phải là giá trị mặc định cho loại đó hay không, đại loại như sau:

if (myArgument == default(T))

Nhưng điều này không được biên dịch vì tôi không đảm bảo rằng T sẽ triển khai toán tử ==. Vì vậy, tôi đã chuyển mã sang đây:

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

Bây giờ điều này biên dịch, nhưng sẽ thất bại nếu myArgument là null, đó là một phần của những gì tôi đang thử nghiệm. Tôi có thể thêm một kiểm tra null rõ ràng như thế này:

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

Bây giờ điều này cảm thấy dư thừa với tôi. ReSharper thậm chí còn gợi ý rằng tôi thay đổi phần myArgument == null thành myArgument == default (T) là nơi tôi bắt đầu. Có cách nào tốt hơn để giải quyết vấn đề này?

Tôi cần hỗ trợ cả loại tham chiếu và loại giá trị.


C # hiện hỗ trợ Toán tử có điều kiện Null , đây là đường tổng hợp cho ví dụ cuối cùng bạn đưa ra. Mã của bạn sẽ trở thành if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU

1
@ wizard07KSU Điều đó không hoạt động đối với các loại giá trị, tức là đánh giá truetrong mọi trường hợp vì Equalssẽ luôn được gọi cho các loại giá trị vì myArgumentkhông thể nulltrong trường hợp này và kết quả của Equals(boolean) sẽ không bao giờ null.
jasper

Có giá trị tương đương gần như trùng lặp (vì vậy không bỏ phiếu để đóng): Toán tử không thể == được áp dụng cho các loại chung trong C #?
GSerg

Câu trả lời:


582

Để tránh quyền anh, cách tốt nhất để so sánh thuốc generic cho sự bình đẳng là với EqualityComparer<T>.Default. Điều này tôn trọng IEquatable<T>(không có quyền anh) cũng như object.Equalsvà xử lý tất cả các Nullable<T>sắc thái "nâng". Vì thế:

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

Điều này sẽ phù hợp:

  • null cho các lớp
  • null (trống) cho Nullable<T>
  • zero / false / etc cho các cấu trúc khác

28
Wow, thật thú vị tối nghĩa! Đây chắc chắn là con đường để đi, mặc dù, kudos.
Nick Farina

1
Chắc chắn là câu trả lời tốt nhất. Không có dòng nguệch ngoạc trong mã của tôi sau khi viết lại để sử dụng giải pháp này.
Nathan Ridley

13
Câu trả lời chính xác! Thậm chí tốt hơn là thêm một phương thức mở rộng cho dòng mã này để bạn có thể truy cập obj.IsDefaultForType ()
rikoe

2
@nawfal trong trường hợp Person, p1.Equals(p2)sẽ phụ thuộc vào việc nó thực hiện IEquatable<Person>trên API công khai hay thông qua triển khai rõ ràng - tức là trình biên dịch có thể thấy một Equals(Person other)phương thức công khai . Tuy nhiên; trong thuốc generic , cùng một IL được sử dụng cho tất cả T; một T1điều xảy ra để thực hiện IEquatable<T1>cần phải được đối xử giống hệt với một T2điều không - vì vậy không, nó sẽ không phát hiện ra một Equals(T1 other)phương thức, ngay cả khi nó tồn tại trong thời gian chạy. Trong cả hai trường hợp, cũng có nullsuy nghĩ về (một trong hai đối tượng). Vì vậy, với generic, tôi sẽ sử dụng mã tôi đã đăng.
Marc Gravell

5
Tôi không thể quyết định nếu câu trả lời này đẩy tôi ra xa hoặc gần hơn với sự điên rồ. +1
Steven Liekens

118

Còn cái này thì sao:

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

Sử dụng static object.Equals()phương pháp này giúp bạn không cần phải nulltự kiểm tra. Hoàn toàn đủ điều kiện cuộc gọi với object.có lẽ không cần thiết tùy thuộc vào ngữ cảnh của bạn, nhưng tôi thường staticgọi tiền tố với tên loại chỉ để làm cho mã dễ hòa tan hơn.


2
Bạn thậm chí có thể thả "đối tượng." một phần vì nó dư thừa. if (Bằng (myArgument, mặc định (T)))
Stefan Moser

13
Đúng, thông thường là vậy, nhưng có thể không phụ thuộc vào bối cảnh. Có thể có một phương thức Equals () có hai đối số. Tôi có xu hướng rõ ràng tiền tố tất cả các cuộc gọi tĩnh với tên lớp, nếu chỉ để làm cho mã dễ đọc hơn.
Kent Boogaart

8
Cần lưu ý rằng nó sẽ gây ra quyền anh và trong một số trường hợp nó có thể quan trọng
nightcoder

2
Đối với tôi, điều này không hoạt động khi sử dụng các số nguyên đã được đóng hộp. Bởi vì sau đó nó sẽ là một đối tượng và mặc định cho đối tượng là null thay vì 0.
riezebosch

28

Tôi đã có thể định vị một bài viết Microsoft Connect thảo luận về vấn đề này một cách chi tiết:

Thật không may, hành vi này là theo thiết kế và không có một giải pháp dễ dàng nào cho phép sử dụng với các tham số loại có thể chứa các loại giá trị.

Nếu các loại được biết là loại tham chiếu, thì quá tải mặc định được xác định trên các biến kiểm tra đối tượng cho đẳng thức tham chiếu, mặc dù một loại có thể chỉ định quá tải tùy chỉnh của chính nó. Trình biên dịch xác định quá tải nào sẽ sử dụng dựa trên loại tĩnh của biến (xác định không phải là đa hình). Do đó, nếu bạn thay đổi ví dụ của mình để ràng buộc tham số loại chung T thành loại tham chiếu không được niêm phong (chẳng hạn như Ngoại lệ), trình biên dịch có thể xác định tình trạng quá tải cụ thể để sử dụng và đoạn mã sau sẽ biên dịch:

public class Test<T> where T : Exception

Nếu các loại được biết là loại giá trị, thực hiện kiểm tra công bằng giá trị cụ thể dựa trên các loại chính xác được sử dụng. Không có so sánh "mặc định" tốt ở đây vì các so sánh tham chiếu không có ý nghĩa đối với các loại giá trị và trình biên dịch không thể biết so sánh giá trị cụ thể nào để phát ra. Trình biên dịch có thể phát ra một cuộc gọi đến ValueType.Equals (Object) nhưng phương thức này sử dụng sự phản chiếu và khá kém hiệu quả so với các so sánh giá trị cụ thể. Do đó, ngay cả khi bạn chỉ định ràng buộc kiểu giá trị trên T, không có gì hợp lý để trình biên dịch tạo ở đây:

public class Test<T> where T : struct

Trong trường hợp bạn đã trình bày, trong đó trình biên dịch thậm chí không biết liệu T là giá trị hay loại tham chiếu, thì không có gì tương tự để tạo ra sẽ hợp lệ cho tất cả các loại có thể. So sánh tham chiếu sẽ không hợp lệ đối với các loại giá trị và một số loại so sánh giá trị sẽ gây bất ngờ cho các loại tham chiếu không quá tải.

Dưới đây là những gì bạn có thể làm...

Tôi đã xác nhận rằng cả hai phương thức này đều hoạt động để so sánh chung các loại giá trị tham chiếu và giá trị:

object.Equals(param, default(T))

hoặc là

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

Để so sánh với toán tử "==", bạn sẽ cần sử dụng một trong các phương pháp sau:

Nếu tất cả các trường hợp T xuất phát từ một lớp cơ sở đã biết, bạn có thể cho trình biên dịch biết bằng cách sử dụng các hạn chế loại chung.

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

Trình biên dịch sau đó nhận ra cách thực hiện các thao tác trên MyBase và sẽ không ném "Toán tử '==' cho các toán hạng loại 'T' và 'T'" mà bạn hiện đang thấy.

Một lựa chọn khác là hạn chế T đối với bất kỳ loại nào thực hiện IComparable.

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

Và sau đó sử dụng CompareTophương thức được xác định bởi giao diện IComparable .


4
"hành vi này là theo thiết kế và không có một giải pháp dễ dàng nào cho phép sử dụng với các tham số loại có thể chứa các loại giá trị." Thật ra Microsoft đã sai. Có một giải pháp dễ dàng: MS nên mở rộng mã opq ceq để hoạt động trên các loại giá trị dưới dạng toán tử bitwise. Sau đó, họ có thể cung cấp một nội tại chỉ đơn giản là sử dụng opcode này, ví dụ object.BitwiseOrReferenceEquals <T> (value, default (T)) chỉ đơn giản sử dụng ceq. Đối với cả hai loại giá trị và tham chiếu, điều này sẽ kiểm tra sự bằng nhau của giá trị bit (nhưng đối với các loại tham chiếu, đẳng thức bit theo tham chiếu giống như đối
tượng.ReferenceEquals

1
Tôi nghĩ rằng liên kết Microsoft Connect mà bạn muốn là connect.microsoft.com/VisualStudio/feedback/details/304501/
mẹo

18

Thử cái này:

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

nên biên dịch, và làm những gì bạn muốn.


Không phải là <code> mặc định (T) </ code>? <code> EqualityComparer <T> .Default.Equals (myArgument) </ code> nên thực hiện thủ thuật.
Joshcodes

2
1) bạn đã thử nó chưa, và 2) bạn đang so sánh với cái gì, đối tượng so sánh? Các Equalsphương pháp IEqualityComparernhận hai đối số, hai đối tượng để so sánh, vì vậy không, nó không phải là không cần thiết.
Lasse V. Karlsen

Điều này thậm chí còn tốt hơn câu trả lời được chấp nhận IMHO bởi vì nó xử lý đấm bốc / unboxing và các loại khác. Xem câu trả lời của câu hỏi "đóng dưới dạng dupe" này: stackoverflow.com/a/864860/210780
tro999

7

(Đã chỉnh sửa)

Marc Gravell có câu trả lời tốt nhất, nhưng tôi muốn đăng một đoạn mã đơn giản mà tôi đã làm việc để chứng minh điều đó. Chỉ cần chạy nó trong một ứng dụng bảng điều khiển C # đơn giản:

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

Một điều nữa: ai đó với VS2008 có thể thử điều này như một phương thức mở rộng không? Tôi bị mắc kẹt với năm 2005 ở đây và tôi tò mò muốn xem liệu điều đó có được phép không.


Chỉnh sửa: Đây là cách để làm cho nó hoạt động như một phương thức mở rộng:

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

3
Nó "hoạt động" như một phương pháp mở rộng. Điều này thật thú vị vì nó hoạt động ngay cả khi bạn nói o.IsDefault <object> () khi o là null. Đáng sợ =)
Nick Farina

6

Để xử lý tất cả các loại T, bao gồm cả T là loại nguyên thủy, bạn sẽ cần biên dịch theo cả hai phương pháp so sánh:

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

1
Lưu ý rằng chức năng đã được thay đổi để chấp nhận Func <T> và trả về T, mà tôi nghĩ đã vô tình bị bỏ sót khỏi mã của người hỏi.
Nick Farina

Có vẻ ReSharper đang nhắn tin cho tôi. Không nhận ra cảnh báo của nó về việc so sánh có thể có giữa loại giá trị và null không phải là cảnh báo của trình biên dịch.
Nathan Ridley

2
FYI: Nếu T hóa ra là một loại giá trị thì việc so sánh với null sẽ được xử lý là luôn luôn sai bởi jitter.
Eric Lippert

Có ý nghĩa - thời gian chạy sẽ so sánh một con trỏ với một loại giá trị. Tuy nhiên, kiểm tra Equals () không hoạt động trong trường hợp đó (thật thú vị, vì có vẻ như ngôn ngữ rất năng động để nói 5.Equals (4) không biên dịch).
Nick Farina

2
Xem câu trả lời EqualityComparer <T> để biết giải pháp thay thế không liên quan đến quyền anh et
Marc Gravell

2

Sẽ có một vấn đề ở đây -

Nếu bạn sẽ cho phép điều này hoạt động với mọi loại, mặc định (T) sẽ luôn là null cho các loại tham chiếu và 0 (hoặc struct đầy 0) cho các loại giá trị.

Tuy nhiên, đây có lẽ không phải là hành vi của bạn. Nếu bạn muốn điều này hoạt động theo cách chung, có lẽ bạn cần sử dụng sự phản chiếu để kiểm tra loại T và xử lý các loại giá trị khác với các loại tham chiếu.

Ngoài ra, bạn có thể đặt một ràng buộc giao diện cho điều này và giao diện có thể cung cấp một cách để kiểm tra đối với mặc định của lớp / struct.


1

Tôi nghĩ có lẽ bạn cần chia logic này thành hai phần và kiểm tra null trước.

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

Trong phương thức IsNull, chúng tôi dựa vào thực tế là các đối tượng ValueType không thể là null theo định nghĩa vì vậy nếu giá trị xảy ra là một lớp xuất phát từ ValueType, chúng tôi đã biết nó không phải là null. Mặt khác, nếu đó không phải là một loại giá trị thì chúng ta chỉ có thể so sánh giá trị được truyền với một đối tượng với null. Chúng ta có thể tránh việc kiểm tra đối với ValueType bằng cách chuyển thẳng tới một đối tượng, nhưng điều đó có nghĩa là một loại giá trị sẽ được đóng hộp, đó là điều mà chúng ta có thể muốn tránh vì nó ngụ ý rằng một đối tượng mới được tạo trên heap.

Trong phương thức IsNullOrEmpty, chúng tôi đang kiểm tra trường hợp đặc biệt của chuỗi. Đối với tất cả các loại khác, chúng tôi đang so sánh giá trị (điều đã biết là không giá trị) với giá trị mặc định của nó đối với tất cả các loại tham chiếu là null và đối với các loại giá trị thường là một dạng số không (nếu chúng là tích phân).

Sử dụng các phương thức này, đoạn mã sau hoạt động như bạn mong đợi:

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) ...
}

1

Phương pháp mở rộng dựa trên câu trả lời được chấp nhận.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Sử dụng:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Thay thế bằng null để đơn giản hóa:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Sử dụng:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }

0

Tôi sử dụng:

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

-1

Không biết điều này có phù hợp với yêu cầu của bạn hay không, nhưng bạn có thể buộc T trở thành Loại thực hiện giao diện như IComparable và sau đó sử dụng phương thức ComparesTo () từ giao diện đó (mà IIRC hỗ trợ / xử lý null) như thế này :

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

Có thể có các giao diện khác mà bạn có thể sử dụng cũng như IEquitable, v.v.


OP đang lo lắng về NullReferenceException và bạn đang đảm bảo anh ta như vậy.
nawfal

-2

@ilititi:

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

Toán tử '==' không thể được áp dụng cho toán hạng loại 'T' và 'T'

Tôi không thể nghĩ ra một cách để làm điều này mà không cần kiểm tra null rõ ràng theo sau bằng cách gọi phương thức Equals hoặc object.Equals như đề xuất ở trên.

Bạn có thể nghĩ ra một giải pháp bằng System.Comparison nhưng thực sự điều đó sẽ kết thúc với cách nhiều dòng mã hơn và tăng độ phức tạp đáng kể.


-3

Tôi nghĩ rằng bạn đã gần gũi.

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

Bây giờ điều này biên dịch, nhưng sẽ thất bại nếu myArgumentlà null, đó là một phần của những gì tôi đang thử nghiệm. Tôi có thể thêm một kiểm tra null rõ ràng như thế này:

Bạn chỉ cần đảo ngược đối tượng mà các đẳng thức đang được gọi cho một cách tiếp cận an toàn null thanh lịch.

default(T).Equals(myArgument);

Tôi đã suy nghĩ những điều chính xác.
Chris Gessler

6
mặc định (T) của loại tham chiếu là null và dẫn đến NullReferenceException được bảo đảm.
Stefan Steinegger
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.