it-swarm-vi.tech

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

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 bởi 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 đề nghị 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ả hai loại tham chiếu và loại giá trị.

239
Stefan Moser

Để 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.Equals và xử lý tất cả các sắc thái "nâng" Nullable<T>. 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
486
Marc Gravell

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

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

Sử dụng phương thức static object.Equals() để tránh bạn cần phải tự kiểm tra null. Hoàn toàn đủ điều kiện cuộc gọi với object. có thể 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 gọi tiền tố static với tên loại chỉ để làm cho mã dễ hòa tan hơn.

113
Kent Boogaart

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 mức 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 (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, thì 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 kiểu 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 pháp 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

Sau đó, trình biên dịch sẽ 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 tùy chọn khác là hạn chế T ở 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 phương thức CompareTo được xác định bởi giao diện IComparable .

24
Eric Schoonover

Thử đi:

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

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

18

(Đã 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));
    }
}
7
Joel Coehoorn

Để 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;
    }
6
Nick Farina

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 đối với các loại tham chiếu và 0 (hoặc struct đầy 0) cho các loại giá trị.

Đây có lẽ không phải là hành vi bạn theo đuổi, mặc dù. 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.

2
Reed Copsey

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 so sánh giá trị (mà đã biết là không null) 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 0 (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
Damian Powell

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