Ràng buộc kiểu chung trong C # cho mọi thứ có thể nullable


111

Vì vậy, tôi có lớp học này:

public class Foo<T> where T : ???
{
    private T item;

    public bool IsNull()
    {
        return item == null;
    }

}

Bây giờ tôi đang tìm kiếm một ràng buộc kiểu cho phép tôi sử dụng mọi thứ như tham số kiểu có thể null. Điều đó có nghĩa là tất cả các loại tham chiếu, cũng như tất cả các loại Nullable( T?):

Foo<String> ... = ...
Foo<int?> ... = ...

nên có thể.

Việc sử dụng classlàm ràng buộc kiểu chỉ cho phép tôi sử dụng các kiểu tham chiếu.

Thông tin bổ sung: Tôi đang viết một ứng dụng đường ống và bộ lọc và muốn sử dụng một nulltham chiếu làm mục cuối cùng chuyển vào đường ống, để mọi bộ lọc có thể tắt tốt, thực hiện dọn dẹp, v.v.


1
@Tim không cho phép Nullables
Rik

Liên kết này có thể giúp bạn: social.msdn.microsoft.com/Forums/en-US/…
Réda Mattar

2
Không thể làm điều này trực tiếp. Có lẽ bạn có thể cho chúng tôi biết thêm về kịch bản của bạn? Hoặc có lẽ bạn có thể sử dụng IFoo<T>làm kiểu làm việc và tạo các phiên bản thông qua phương thức gốc? Điều đó có thể được thực hiện để làm việc.
Jon

Tôi không chắc tại sao bạn muốn hoặc cần phải hạn chế điều gì đó theo cách này. Nếu mục đích duy nhất của bạn là biến "if x == null" thành if x.IsNull () "thì điều này có vẻ vô nghĩa và không trực quan đối với 99,99% nhà phát triển đã quen với cú pháp cũ. Trình biên dịch sẽ không cho phép bạn làm" if (int) x == null" anyway, do đó bạn đã được bảo hiểm.
RJ Lohan

1
Điều này được thảo luận khá rộng rãi trên SO. stackoverflow.com/questions/209160/...stackoverflow.com/questions/13794554/...
Maxim Gershkovich

Câu trả lời:


22

Nếu bạn sẵn sàng thực hiện kiểm tra thời gian chạy trong phương thức khởi tạo của Foo thay vì kiểm tra thời gian biên dịch, bạn có thể kiểm tra xem kiểu không phải là tham chiếu hay kiểu nullable và ném một ngoại lệ nếu đúng như vậy.

Tôi nhận thấy rằng việc chỉ kiểm tra thời gian chạy có thể không được chấp nhận, nhưng chỉ trong trường hợp:

public class Foo<T>
{
    private T item;

    public Foo()
    {
        var type = typeof(T);

        if (Nullable.GetUnderlyingType(type) != null)
            return;

        if (type.IsClass)
            return;

        throw new InvalidOperationException("Type is not nullable or reference type.");
    }

    public bool IsNull()
    {
        return item == null;
    }
}

Sau đó, đoạn mã sau sẽ biên dịch, nhưng đoạn mã cuối cùng ( foo3) ném ra một ngoại lệ trong hàm tạo:

var foo1 = new Foo<int?>();
Console.WriteLine(foo1.IsNull());

var foo2 = new Foo<string>();
Console.WriteLine(foo2.IsNull());

var foo3= new Foo<int>();  // THROWS
Console.WriteLine(foo3.IsNull());

31
Nếu bạn đang đi để làm điều này, chắc chắn rằng bạn làm việc kiểm tra trong tĩnh constructor, nếu không bạn sẽ làm chậm tốc độ xây dựng của tất cả các thể hiện của lớp generic của bạn (không cần thiết)
Eamon Nerbonne

2
@EamonNerbonne Bạn không nên nâng cao ngoại lệ từ các trình tạo tĩnh: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
Nguyên tắc không phải là điều tuyệt đối. Nếu bạn muốn kiểm tra này, bạn sẽ phải đánh đổi chi phí của kiểm tra thời gian chạy so với sự không vui của các ngoại lệ trong một hàm tạo tĩnh. Vì bạn đang thực sự triển khai một trình phân tích tĩnh nghèo nàn ở đây, ngoại lệ này sẽ không bao giờ được ném ra ngoại trừ trong quá trình phát triển. Cuối cùng, ngay cả khi bạn muốn tránh các ngoại lệ xây dựng tĩnh bằng mọi giá (không khôn ngoan), thì bạn vẫn nên thực hiện càng nhiều công việc tĩnh càng tốt và càng ít càng tốt trong hàm tạo cá thể - ví dụ bằng cách đặt cờ "isBorked" hoặc bất cứ điều gì.
Eamon Nerbonne

Ngẫu nhiên, tôi không nghĩ bạn nên cố gắng làm điều này chút nào. Trong hầu hết các trường hợp, tôi chỉ muốn chấp nhận điều này như một giới hạn của C #, hơn là thử và làm việc với một bản tóm tắt bị rò rỉ, dễ thất bại. Ví dụ: một giải pháp khác có thể là chỉ yêu cầu các lớp, hoặc chỉ yêu cầu cấu trúc (và rõ ràng là làm cho em có thể vô hiệu hóa) - hoặc làm cả hai và có hai phiên bản. Đó không phải là lời chỉ trích về giải pháp này; chỉ là vấn đề này không thể được giải quyết tốt - trừ khi, nghĩa là bạn sẵn sàng viết một bộ phân tích roslyn tùy chỉnh.
Eamon Nerbonne

1
Bạn có thể tận dụng tốt nhất cả hai thế giới - giữ một static bool isValidTypetrường mà bạn đã đặt trong hàm tạo tĩnh, sau đó chỉ cần kiểm tra cờ đó trong hàm tạo cá thể và ném nếu đó là kiểu không hợp lệ để bạn không phải thực hiện tất cả công việc kiểm tra mỗi khi bạn tạo một ví dụ. Tôi sử dụng mẫu này thường xuyên.
Mike Marynowski

20

Tôi không biết cách triển khai tương đương với OR trong generic. Tuy nhiên, tôi có thể đề xuất sử dụng từ khóa mặc định để tạo null cho các kiểu nullable và giá trị 0 cho các cấu trúc:

public class Foo<T>
{
    private T item;

    public bool IsNullOrDefault()
    {
        return Equals(item, default(T));
    }
}

Bạn cũng có thể triển khai phiên bản Nullable của mình:

class MyNullable<T> where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(MyNullable<T> value)
    {
        return value != null ? value.Value : default(T);
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T> { Value = value };
    }
}

class Foo<T> where T : class
{
    public T Item { get; set; }

    public bool IsNull()
    {
        return Item == null;
    }
}

Thí dụ:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Foo<MyNullable<int>>().IsNull()); // true
        Console.WriteLine(new Foo<MyNullable<int>> {Item = 3}.IsNull()); // false
        Console.WriteLine(new Foo<object>().IsNull()); // true
        Console.WriteLine(new Foo<object> {Item = new object()}.IsNull()); // false

        var foo5 = new Foo<MyNullable<int>>();
        int integer = foo5.Item;
        Console.WriteLine(integer); // 0

        var foo6 = new Foo<MyNullable<double>>();
        double real = foo6.Item;
        Console.WriteLine(real); // 0

        var foo7 = new Foo<MyNullable<double>>();
        foo7.Item = null;
        Console.WriteLine(foo7.Item); // 0
        Console.WriteLine(foo7.IsNull()); // true
        foo7.Item = 3.5;
        Console.WriteLine(foo7.Item); // 3.5
        Console.WriteLine(foo7.IsNull()); // false

        // var foo5 = new Foo<int>(); // Not compile
    }
}

Nullable <T> ban đầu trong khung là một cấu trúc, không phải một lớp. Tôi không nghĩ nên tạo một trình bao bọc kiểu tham chiếu sẽ bắt chước một kiểu giá trị.
Niall Connaughton

1
Đề xuất đầu tiên sử dụng mặc định là hoàn hảo! Bây giờ mẫu của tôi với kiểu chung được trả về có thể trả về giá trị null cho các đối tượng và giá trị mặc định cho các kiểu dựng sẵn.
Casey Anderson

13

Tôi gặp phải vấn đề này vì một trường hợp đơn giản hơn là muốn một phương thức tĩnh chung chung có thể lấy bất kỳ thứ gì "nullable" (hoặc kiểu tham chiếu hoặc Nullables), điều này khiến tôi không có giải pháp thỏa đáng. Vì vậy, tôi đã đưa ra giải pháp của riêng mình tương đối dễ giải hơn so với câu hỏi đã nêu của OP bằng cách chỉ cần có hai phương thức nạp chồng, một phương thức nhận a Tvà có ràng buộc where T : classvà một phương thức khác nhận a T?và có where T : struct.

Sau đó, tôi nhận ra rằng giải pháp đó cũng có thể được áp dụng cho vấn đề này để tạo ra một giải pháp có thể kiểm tra được tại thời điểm biên dịch bằng cách đặt hàm tạo là riêng tư (hoặc được bảo vệ) và sử dụng phương thức nhà máy tĩnh:

    //this class is to avoid having to supply generic type arguments 
    //to the static factory call (see CA1000)
    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return Foo<TFoo>.Create(value);
        }

        public static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return Foo<TFoo?>.Create(value);
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo(T value)
        {
            item = value;
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return new Foo<TFoo>(value);
        }

        internal static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return new Foo<TFoo?>(value);
        }
    }

Bây giờ chúng ta có thể sử dụng nó như thế này:

        var foo1 = new Foo<int>(1); //does not compile
        var foo2 = Foo.Create(2); //does not compile
        var foo3 = Foo.Create(""); //compiles
        var foo4 = Foo.Create(new object()); //compiles
        var foo5 = Foo.Create((int?)5); //compiles

Nếu bạn muốn một phương thức khởi tạo không tham số, bạn sẽ không gặp phải tình trạng quá tải, nhưng bạn vẫn có thể làm như sau:

    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return Foo<TFoo>.Create<TFoo>();
        }

        public static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return Foo<TFoo?>.CreateNullable<TFoo>();
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo()
        {
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return new Foo<TFoo>();
        }

        internal static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return new Foo<TFoo?>();
        }
    }

Và sử dụng nó như thế này:

        var foo1 = new Foo<int>(); //does not compile
        var foo2 = Foo.Create<int>(); //does not compile
        var foo3 = Foo.Create<string>(); //compiles
        var foo4 = Foo.Create<object>(); //compiles
        var foo5 = Foo.CreateNullable<int>(); //compiles

Có một vài nhược điểm đối với giải pháp này, một là bạn có thể thích sử dụng 'mới' để xây dựng các đối tượng. Một là bạn sẽ không thể sử dụng Foo<T>như một đối số kiểu chung chung cho một loại hạn chế của một cái gì đó như: where TFoo: new(). Cuối cùng là một chút mã bổ sung bạn cần ở đây sẽ tăng lên, đặc biệt nếu bạn cần nhiều hàm tạo quá tải.


8

Như đã đề cập, bạn không thể kiểm tra thời gian biên dịch cho nó. Các ràng buộc chung trong .NET thiếu nghiêm trọng và không hỗ trợ hầu hết các trường hợp.

Tuy nhiên, tôi coi đây là một giải pháp tốt hơn để kiểm tra thời gian chạy. Nó có thể được tối ưu hóa tại thời điểm biên dịch JIT, vì cả hai đều là hằng số.

public class SomeClass<T>
{
    public SomeClass()
    {
        // JIT-compile time check, so it doesn't even have to evaluate.
        if (default(T) != null)
            throw new InvalidOperationException("SomeClass<T> requires T to be a nullable type.");

        T variable;
        // This still won't compile
        // variable = null;
        // but because you know it's a nullable type, this works just fine
        variable = default(T);
    }
}

3

Một ràng buộc kiểu như vậy là không thể. Theo tài liệu về các ràng buộc kiểu , không có ràng buộc nào nắm bắt cả kiểu tham chiếu và kiểu nullable. Vì các ràng buộc chỉ có thể được kết hợp trong một tổ hợp, không có cách nào để tạo ra một ràng buộc như vậy bằng tổ hợp.

Tuy nhiên, bạn có thể quay lại tham số kiểu không bị ràng buộc vì bạn luôn có thể kiểm tra == null. Nếu kiểu là một kiểu giá trị, séc sẽ luôn luôn đánh giá là sai. Sau đó, bạn có thể sẽ nhận được cảnh báo R # "Có thể so sánh kiểu giá trị với null", điều này không quan trọng, miễn là ngữ nghĩa phù hợp với bạn.

Một thay thế có thể được sử dụng

object.Equals(value, default(T))

thay vì kiểm tra null, vì mặc định (T) trong đó T: class luôn là null. Tuy nhiên, điều này có nghĩa là bạn không thể phân biệt thời tiết một giá trị không thể nullable chưa bao giờ được đặt rõ ràng hoặc chỉ được đặt thành giá trị mặc định của nó.


Tôi nghĩ rằng vấn đề là làm thế nào để kiểm tra giá trị đó chưa bao giờ được thiết lập. Khác với null dường như chỉ ra rằng giá trị đã được khởi tạo.
Ryszard Dżegan

Điều đó không làm mất hiệu lực của cách tiếp cận, vì các loại giá trị luôn được đặt (ít nhất là ngầm định với giá trị mặc định tương ứng của chúng).
Sven Amann

3

tôi sử dụng

public class Foo<T> where T: struct
{
    private T? item;
}

-2
    public class Foo<T>
    {
        private T item;

        public Foo(T item)
        {
            this.item = item;
        }

        public bool IsNull()
        {
            return object.Equals(item, null);
        }
    }

    var fooStruct = new Foo<int?>(3);
        var b = fooStruct.IsNull();

        var fooStruct1 = new Foo<int>(3);
        b = fooStruct1.IsNull();

        var fooStruct2 = new Foo<int?>(null);
        b = fooStruct2.IsNull();

        var fooStruct3 = new Foo<string>("qqq");
        b = fooStruct3.IsNull();

        var fooStruct4 = new Foo<string>(null);
        b = fooStruct4.IsNull();

Cách nhập này cho phép Foo mới <int> (42) và IsNull () sẽ trả về false, mặc dù đúng về mặt ngữ nghĩa, nhưng không có ý nghĩa đặc biệt.
RJ Lohan

1
42 là "Câu trả lời cho câu hỏi cuối cùng về sự sống, vũ trụ và vạn vật". Nói một cách đơn giản: IsNull cho mọi giá trị int sẽ trả về false (ngay cả với giá trị 0).
Ryszard Dżegan
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.