Công đoàn phân biệt đối xử trong C #


92

[Lưu ý: Câu hỏi này có tiêu đề ban đầu là " C (ish) style union in C # " nhưng như nhận xét của Jeff đã cho tôi biết, rõ ràng cấu trúc này được gọi là 'liên hiệp phân biệt đối xử']

Xin lỗi về sự dài dòng của câu hỏi này.

Có một vài câu hỏi nghe tương tự với tôi đã có trong SO nhưng chúng dường như tập trung vào lợi ích tiết kiệm bộ nhớ của liên minh hoặc sử dụng nó cho tương tác. Đây là một ví dụ về một câu hỏi như vậy .

Mong muốn của tôi là có một loại hình công đoàn hơi khác.

Tôi đang viết một số mã tại thời điểm này tạo ra các đối tượng trông giống như thế này

public class ValueWrapper
{
    public DateTime ValueCreationDate;
    // ... other meta data about the value

    public object ValueA;
    public object ValueB;
}

Những thứ khá phức tạp tôi nghĩ bạn sẽ đồng ý. Vấn đề là nó ValueAchỉ có thể thuộc một số loại nhất định (giả sử string, intFoo(đó là một lớp) và ValueBcó thể là một nhóm nhỏ các loại khác. Tôi không thích coi những giá trị này là đối tượng (tôi muốn cảm giác ấm áp vừa khít của mã hóa với một chút về kiểu an toàn).

Vì vậy, tôi đã nghĩ đến việc viết một lớp trình bao bọc nhỏ tầm thường để thể hiện sự thật rằng ValueA một cách hợp lý là một tham chiếu đến một kiểu cụ thể. Tôi gọi cho lớp học Unionvì những gì tôi đang cố gắng đạt được nhắc nhở tôi về khái niệm công đoàn trong C.

public class Union<A, B, C>
{
    private readonly Type type; 
    public readonly A a;
    public readonly B b;
    public readonly C c;

    public A A{get {return a;}}
    public B B{get {return b;}}
    public C C{get {return c;}}

    public Union(A a)
    {
        type = typeof(A);
        this.a = a;
    }

    public Union(B b)
    {
        type = typeof(B);
        this.b = b;
    }

    public Union(C c)
    {
        type = typeof(C);
        this.c = c;
    }

    /// <summary>
    /// Returns true if the union contains a value of type T
    /// </summary>
    /// <remarks>The type of T must exactly match the type</remarks>
    public bool Is<T>()
    {
        return typeof(T) == type;
    }

    /// <summary>
    /// Returns the union value cast to the given type.
    /// </summary>
    /// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>
    public T As<T>()
    {
        if(Is<A>())
        {
            return (T)(object)a;    // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types? 
            //return (T)x;          // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."
        }

        if(Is<B>())
        {
            return (T)(object)b; 
        }

        if(Is<C>())
        {
            return (T)(object)c; 
        }

        return default(T);
    }
}

Sử dụng lớp ValueWrapper này bây giờ trông như thế này

public class ValueWrapper2
{
    public DateTime ValueCreationDate;
    public  Union<int, string, Foo> ValueA;
    public  Union<double, Bar, Foo> ValueB;
}

đó là một cái gì đó giống như những gì tôi muốn đạt được nhưng tôi thiếu một yếu tố khá quan trọng - đó là kiểm tra kiểu bắt buộc của trình biên dịch khi gọi hàm Is và As như đoạn mã sau minh họa

    public void DoSomething()
    {
        if(ValueA.Is<string>())
        {
            var s = ValueA.As<string>();
            // .... do somethng
        }

        if(ValueA.Is<char>()) // I would really like this to be a compile error
        {
            char c = ValueA.As<char>();
        }
    }

IMO Không hợp lệ khi hỏi ValueA nếu đó là một charvì định nghĩa của nó nói rõ ràng là không - đây là một lỗi lập trình và tôi muốn trình biên dịch khắc phục lỗi này. [Ngoài ra nếu tôi có thể hiểu đúng điều này thì (hy vọng) tôi cũng sẽ nhận được intellisense - đó sẽ là một lợi ích.]

Để đạt được điều này, tôi muốn nói với trình biên dịch rằng kiểu Tcó thể là một trong A, B hoặc C

    public bool Is<T>() where T : A 
                           or T : B // Yes I know this is not legal!
                           or T : C 
    {
        return typeof(T) == type;
    } 

Có ai có bất kỳ ý tưởng nếu những gì tôi muốn đạt được là có thể? Hay tôi chỉ đơn giản là ngu ngốc khi viết lớp này ngay từ đầu?

Cảm ơn trước.


3
Unions trong C có thể được triển khai trong C # cho các kiểu giá trị sử dụng StructLayout(LayoutKind.Explicit)FieldOffset. Tất nhiên, điều này không thể được thực hiện với các loại tham chiếu. Những gì bạn đang làm không giống C Union chút nào.
Brian

4
Đây thường được gọi là công đoàn bị phân biệt đối xử .
Jeff Hardy

Cảm ơn Jeff - Tôi đã không biết gì về thuật ngữ này nhưng đây là khá nhiều một cách chính xác những gì tôi muốn đạt được
Chris Fewtrell

7
Có thể không phải là loại phản hồi bạn đang tìm kiếm, nhưng bạn đã xem xét F # chưa? Nó có các hợp nhất an toàn với kiểu và khớp mẫu được nướng ngay trong ngôn ngữ, dễ dàng hơn để đại diện cho các liên kết so với C #.
Juliet

1
Một tên khác của liên hiệp phân biệt là một loại tổng.
cdiggins

Câu trả lời:


113

Tôi không thực sự thích các giải pháp kiểm tra kiểu và truyền kiểu được cung cấp ở trên, vì vậy đây là 100% type-safe union sẽ gây ra lỗi biên dịch nếu bạn cố gắng sử dụng kiểu dữ liệu sai:

using System;

namespace Juliet
{
    class Program
    {
        static void Main(string[] args)
        {
            Union3<int, char, string>[] unions = new Union3<int,char,string>[]
                {
                    new Union3<int, char, string>.Case1(5),
                    new Union3<int, char, string>.Case2('x'),
                    new Union3<int, char, string>.Case3("Juliet")
                };

            foreach (Union3<int, char, string> union in unions)
            {
                string value = union.Match(
                    num => num.ToString(),
                    character => new string(new char[] { character }),
                    word => word);
                Console.WriteLine("Matched union with value '{0}'", value);
            }

            Console.ReadLine();
        }
    }

    public abstract class Union3<A, B, C>
    {
        public abstract T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h);
        // private ctor ensures no external classes can inherit
        private Union3() { } 

        public sealed class Case1 : Union3<A, B, C>
        {
            public readonly A Item;
            public Case1(A item) : base() { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return f(Item);
            }
        }

        public sealed class Case2 : Union3<A, B, C>
        {
            public readonly B Item;
            public Case2(B item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return g(Item);
            }
        }

        public sealed class Case3 : Union3<A, B, C>
        {
            public readonly C Item;
            public Case3(C item) { this.Item = item; }
            public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
            {
                return h(Item);
            }
        }
    }
}

3
Đúng vậy, nếu bạn muốn các công đoàn phân biệt đối xử an toàn, bạn sẽ cần match, và đó là một cách tốt để có được điều đó.
Pavel Minaev

20
Và nếu tất cả mã soạn sẵn đó khiến bạn thất vọng, bạn có thể thử triển khai này, thay vào đó, gắn thẻ các trường hợp một cách rõ ràng: pastebin.com/EEdvVh2R . Ngẫu nhiên, phong cách này rất giống với cách F # và OCaml đại diện cho các công đoàn trong nội bộ.
Juliet

4
Tôi thích mã ngắn hơn của Juliet, nhưng nếu các loại là <int, int, string> thì sao? Bạn sẽ gọi hàm tạo thứ hai như thế nào?
Robert Jeppesen

2
Tôi không biết làm thế nào mà cái này không có 100 lượt ủng hộ. Đó là một điều của vẻ đẹp!
Paolo Falabella

5
@nexus xem xét loại này trong F #:type Result = Success of int | Error of int
AlexFoxGill

33

Tôi thích hướng của giải pháp được chấp nhận nhưng nó không mở rộng quy mô tốt cho các liên hợp có nhiều hơn ba mục (ví dụ: một liên hợp gồm 9 mục sẽ yêu cầu 9 định nghĩa lớp).

Đây là một cách tiếp cận khác cũng là kiểu an toàn 100% tại thời điểm biên dịch, nhưng cách đó dễ phát triển thành các công ty lớn.

public class UnionBase<A>
{
    dynamic value;

    public UnionBase(A a) { value = a; } 
    protected UnionBase(object x) { value = x; }

    protected T InternalMatch<T>(params Delegate[] ds)
    {
        var vt = value.GetType();    
        foreach (var d in ds)
        {
            var mi = d.Method;

            // These are always true if InternalMatch is used correctly.
            Debug.Assert(mi.GetParameters().Length == 1);
            Debug.Assert(typeof(T).IsAssignableFrom(mi.ReturnType));

            var pt = mi.GetParameters()[0].ParameterType;
            if (pt.IsAssignableFrom(vt))
                return (T)mi.Invoke(null, new object[] { value });
        }
        throw new Exception("No appropriate matching function was provided");
    }

    public T Match<T>(Func<A, T> fa) { return InternalMatch<T>(fa); }
}

public class Union<A, B> : UnionBase<A>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb) { return InternalMatch<T>(fa, fb); }
}

public class Union<A, B, C> : Union<A, B>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc) { return InternalMatch<T>(fa, fb, fc); }
}

public class Union<A, B, C, D> : Union<A, B, C>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd) { return InternalMatch<T>(fa, fb, fc, fd); }
}

public class Union<A, B, C, D, E> : Union<A, B, C, D>
{
    public Union(A a) : base(a) { }
    public Union(B b) : base(b) { }
    public Union(C c) : base(c) { }
    public Union(D d) : base(d) { }
    public Union(E e) : base(e) { }
    protected Union(object x) : base(x) { }
    public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd, Func<E, T> fe) { return InternalMatch<T>(fa, fb, fc, fd, fe); }
}

public class DiscriminatedUnionTest : IExample
{
    public Union<int, bool, string, int[]> MakeUnion(int n)
    {
        return new Union<int, bool, string, int[]>(n);
    }

    public Union<int, bool, string, int[]> MakeUnion(bool b)
    {
        return new Union<int, bool, string, int[]>(b);
    }

    public Union<int, bool, string, int[]> MakeUnion(string s)
    {
        return new Union<int, bool, string, int[]>(s);
    }

    public Union<int, bool, string, int[]> MakeUnion(params int[] xs)
    {
        return new Union<int, bool, string, int[]>(xs);
    }

    public void Print(Union<int, bool, string, int[]> union)
    {
        var text = union.Match(
            n => "This is an int " + n.ToString(),
            b => "This is a boolean " + b.ToString(),
            s => "This is a string" + s,
            xs => "This is an array of ints " + String.Join(", ", xs));
        Console.WriteLine(text);
    }

    public void Run()
    {
        Print(MakeUnion(1));
        Print(MakeUnion(true));
        Print(MakeUnion("forty-two"));
        Print(MakeUnion(0, 1, 1, 2, 3, 5, 8));
    }
}

+1 Điều này sẽ nhận được nhiều phê duyệt hơn; Tôi thích cách bạn đã làm cho nó đủ linh hoạt để cho phép các tổ chức từ thiện thuộc tất cả các loại.
Paul d'Aoust

+1 cho tính linh hoạt và ngắn gọn của giải pháp của bạn. Tuy nhiên, có một số chi tiết làm tôi bận tâm. Tôi sẽ đăng từng người dưới dạng một nhận xét riêng biệt:
stakx - không còn đóng góp nữa vào

1
1. Việc sử dụng phản ánh có thể phải chịu một hình phạt hiệu suất quá lớn trong một số trường hợp, vì các công đoàn bị phân biệt đối xử, do bản chất cơ bản của chúng, có thể được sử dụng rất thường xuyên.
stakx - không còn đóng góp vào

4
2. Việc sử dụng dynamic& generics trong UnionBase<A>và chuỗi kế thừa dường như không cần thiết. Tạo UnionBase<A>không chung chung, giết hàm tạo bằng cách lấy một Avà tạo valuemột object(dù sao nó cũng là một; không có lợi ích bổ sung khi khai báo nó dynamic). Sau đó dẫn xuất từng Union<…>lớp trực tiếp từ UnionBase. Điều này có lợi thế mà chỉ có Match<T>(…)phương pháp thích hợp sẽ được tiếp xúc. (Như hiện nay, ví dụ như Union<A, B>cho thấy một tình trạng quá tải Match<T>(Func<A, T> fa)mà được đảm bảo để ném một ngoại lệ nếu giá trị kèm theo không phải là một AĐó không nên xảy ra..)
stakx - không còn góp

3
Bạn có thể tìm thấy thư viện của tôi oneof hữu ích, nó nhiều hơn hoặc ít hơn này, nhưng là trên NuGet :) github.com/mcintyre321/OneOf
mcintyre321

20

Tôi đã viết một số bài đăng trên blog về chủ đề này có thể hữu ích:

Giả sử bạn có một kịch bản giỏ hàng với ba trạng thái: "Hết", "Hoạt động" và "Đã thanh toán", mỗi trạng thái có hành vi khác nhau .

  • Bạn tạo ra một ICartStategiao diện mà tất cả các trạng thái đều có điểm chung (và nó có thể chỉ là một giao diện đánh dấu trống)
  • Bạn tạo ba lớp triển khai giao diện đó. (Các lớp không nhất thiết phải ở trong mối quan hệ kế thừa)
  • Giao diện chứa một phương thức "gấp", theo đó bạn chuyển một lambda vào cho từng trạng thái hoặc trường hợp mà bạn cần xử lý.

Bạn có thể sử dụng thời gian chạy F # từ C # nhưng để thay thế trọng lượng nhẹ hơn, tôi đã viết một mẫu T4 nhỏ để tạo mã như thế này.

Đây là giao diện:

partial interface ICartState
{
  ICartState Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        );
}

Và đây là cách thực hiện:

class CartStateEmpty : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the empty state, so invoke cartStateEmpty 
      return cartStateEmpty(this);
  }
}

class CartStateActive : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the active state, so invoke cartStateActive
      return cartStateActive(this);
  }
}

class CartStatePaid : ICartState
{
  ICartState ICartState.Transition(
        Func<CartStateEmpty, ICartState> cartStateEmpty,
        Func<CartStateActive, ICartState> cartStateActive,
        Func<CartStatePaid, ICartState> cartStatePaid
        )
  {
        // I'm the paid state, so invoke cartStatePaid
      return cartStatePaid(this);
  }
}

Bây giờ giả sử bạn mở rộng CartStateEmptyCartStateActivevới một AddItemphương thức không được triển khai bởi CartStatePaid.

Và cũng giả sử rằng CartStateActivecó một Payphương pháp mà các tiểu bang khác không có.

Sau đó, đây là một số mã hiển thị nó đang được sử dụng - thêm hai mặt hàng và sau đó thanh toán cho giỏ hàng:

public ICartState AddProduct(ICartState currentState, Product product)
{
    return currentState.Transition(
        cartStateEmpty => cartStateEmpty.AddItem(product),
        cartStateActive => cartStateActive.AddItem(product),
        cartStatePaid => cartStatePaid // not allowed in this case
        );

}

public void Example()
{
    var currentState = new CartStateEmpty() as ICartState;

    //add some products 
    currentState = AddProduct(currentState, Product.ProductX);
    currentState = AddProduct(currentState, Product.ProductY);

    //pay 
    const decimal paidAmount = 12.34m;
    currentState = currentState.Transition(
        cartStateEmpty => cartStateEmpty,  // not allowed in this case
        cartStateActive => cartStateActive.Pay(paidAmount),
        cartStatePaid => cartStatePaid     // not allowed in this case
        );
}    

Lưu ý rằng mã này hoàn toàn an toàn - không có điều kiện đúc hoặc điều kiện ở bất kỳ đâu và lỗi trình biên dịch nếu bạn cố gắng thanh toán cho một giỏ hàng trống.


Trường hợp sử dụng thú vị. Đối với tôi, việc thực hiện các công đoàn phân biệt đối xử trên bản thân các đối tượng khá dài dòng. Đây là một giải pháp thay thế kiểu chức năng sử dụng các biểu thức chuyển đổi, dựa trên mô hình của bạn: gist.github.com/dcuccia/4029f1cddd7914dc1ae676d8c4af7866 . Bạn có thể thấy rằng DU không thực sự cần thiết nếu chỉ có một đường dẫn "hạnh phúc", nhưng chúng trở nên rất hữu ích khi một phương thức có thể trả về kiểu này hay kiểu khác, tùy thuộc vào các quy tắc logic nghiệp vụ.
David Cuccia

12

Tôi đã viết một thư viện để thực hiện việc này tại https://github.com/mcintyre321/OneOf

Cài đặt-Gói OneOf

Nó có các kiểu chung để thực hiện các DU, ví dụ như OneOf<T0, T1>tất cả các cách OneOf<T0, ..., T9>. Mỗi một trong số đó có một .Matchvà một .Switchcâu lệnh mà bạn có thể sử dụng cho hành vi được đánh máy an toàn của trình biên dịch, ví dụ:

``

OneOf<string, ColorName, Color> backgroundColor = getBackground(); 
Color c = backgroundColor.Match(
    str => CssHelper.GetColorFromString(str),
    name => new Color(name),
    col => col
);

``


7

Tôi không chắc mình hoàn toàn hiểu mục tiêu của bạn. Trong C, một liên minh là một cấu trúc sử dụng cùng một vị trí bộ nhớ cho nhiều trường. Ví dụ:

typedef union
{
    float real;
    int scalar;
} floatOrScalar;

Các floatOrScalarđoàn thể được sử dụng như một chiếc phao, hoặc một int, nhưng cả hai đều tiêu thụ không gian bộ nhớ tương tự. Thay đổi một thay đổi khác. Bạn có thể đạt được điều tương tự với cấu trúc trong C #:

[StructLayout(LayoutKind.Explicit)]
struct FloatOrScalar
{
    [FieldOffset(0)]
    public float Real;
    [FieldOffset(0)]
    public int Scalar;
}

Cấu trúc trên sử dụng tổng số 32bits, thay vì 64bits. Điều này chỉ có thể với một cấu trúc. Ví dụ của bạn ở trên là một lớp và với bản chất của CLR, không đảm bảo về hiệu quả bộ nhớ. Nếu bạn thay đổi một Union<A, B, C>từ loại này sang loại khác, bạn không nhất thiết phải sử dụng lại bộ nhớ ... rất có thể, bạn đang phân bổ một loại mới trên heap và thả một con trỏ khác vào trường sao lưu object. Trái ngược với một liên minh thực sự , cách tiếp cận của bạn thực sự có thể gây ra nhiều sự cố dồn dập hơn những gì bạn sẽ nhận được nếu không sử dụng loại Liên minh của mình.


Như tôi đã đề cập trong câu hỏi của mình, động lực của tôi không phải là hiệu quả ghi nhớ tốt hơn. Tôi đã thay đổi tiêu đề câu hỏi để phản ánh tốt hơn những gì mục tiêu của tôi là - danh hiệu ban đầu của "C (ish) công đoàn" là trong nhận thức muộn màng gây hiểu lầm
Chris Fewtrell

Một công đoàn bị phân biệt đối xử có ý nghĩa hơn rất nhiều đối với những gì bạn đang cố gắng làm. Đối với việc kiểm tra thời gian biên dịch, tôi sẽ xem xét .NET 4 và Hợp đồng mã. Với Hợp đồng mã, có thể thực thi một Hợp đồng thời gian biên dịch. Yêu cầu thực thi các yêu cầu của bạn trên toán tử .Is <T>.
jrista

Tôi đoán rằng tôi vẫn phải đặt câu hỏi về việc sử dụng Union, nói chung. Ngay cả trong C / C ++, các hợp nhất là một thứ rủi ro và phải được sử dụng hết sức cẩn thận. Tôi tò mò tại sao bạn cần đưa một cấu trúc như vậy vào C # ... bạn nhận thấy giá trị gì khi nhận được từ nó?
jrista

2
char foo = 'B';

bool bar = foo is int;

Điều này dẫn đến một cảnh báo, không phải là một lỗi. Nếu bạn đang tìm kiếm các hàm Isvà của bạn Asđể trở thành các hàm tương tự cho các toán tử C #, thì dù sao thì bạn cũng không nên hạn chế chúng theo cách đó.


2

Nếu bạn cho phép nhiều kiểu, bạn không thể đạt được an toàn kiểu (trừ khi các kiểu có liên quan với nhau).

Bạn không thể và sẽ không đạt được bất kỳ loại an toàn kiểu nào, bạn chỉ có thể đạt được byte-giá trị-an toàn bằng cách sử dụng FieldOffset.

Sẽ có ý nghĩa hơn nhiều nếu có một chung ValueWrapper<T1, T2>với T1 ValueAT2 ValueB, ...

Tái bút: khi nói về kiểu-an toàn, tôi muốn nói đến kiểu-an toàn trong thời gian biên dịch.

Nếu bạn cần một trình bao bọc mã (thực hiện logic kinh doanh trên các sửa đổi, bạn có thể sử dụng một cái gì đó dọc theo các dòng:

public class Wrapper
{
    public ValueHolder<int> v1 = 5;
    public ValueHolder<byte> v2 = 8;
}

public struct ValueHolder<T>
    where T : struct
{
    private T value;

    public ValueHolder(T value) { this.value = value; }

    public static implicit operator T(ValueHolder<T> valueHolder) { return valueHolder.value; }
    public static implicit operator ValueHolder<T>(T value) { return new ValueHolder<T>(value); }
}

Để có một cách thoát dễ dàng, bạn có thể sử dụng (nó có vấn đề về hiệu suất, nhưng nó rất đơn giản):

public class Wrapper
{
    private object v1;
    private object v2;

    public T GetValue1<T>() { if (v1.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v1; }
    public void SetValue1<T>(T value) { v1 = value; }

    public T GetValue2<T>() { if (v2.GetType() != typeof(T)) throw new InvalidCastException(); return (T)v2; }
    public void SetValue2<T>(T value) { v2 = value; }
}

//usage:
Wrapper wrapper = new Wrapper();
wrapper.SetValue1("aaaa");
wrapper.SetValue2(456);

string s = wrapper.GetValue1<string>();
DateTime dt = wrapper.GetValue1<DateTime>();//InvalidCastException

Đề xuất của bạn về việc làm cho ValueWrapper chung chung có vẻ giống như câu trả lời rõ ràng nhưng nó khiến tôi gặp vấn đề trong những gì tôi đang làm. Về cơ bản, mã của tôi đang tạo các đối tượng trình bao bọc này bằng cách phân tích cú pháp một số dòng văn bản. Vì vậy, tôi có một phương thức như ValueWrapper MakeValueWrapper (văn bản chuỗi). Nếu tôi tạo trình bao bọc chung chung thì tôi cần thay đổi chữ ký của MakeValueWrapper thành chữ ký chung và điều này đến lượt nó có nghĩa là mã gọi cần biết loại nào được mong đợi và tôi chỉ không biết điều này trước khi tôi phân tích văn bản. ...
Chris Poortrell

... nhưng ngay cả khi tôi đang viết nhận xét cuối cùng, có lẽ tôi đã bỏ lỡ điều gì đó (hoặc nhầm lẫn điều gì đó) bởi vì những gì tôi đang cố gắng làm không cảm thấy khó khăn như tôi đang làm. Tôi nghĩ mình sẽ quay lại và dành vài phút làm việc trên trình bao bọc đã được tổng quát hóa và xem liệu tôi có thể điều chỉnh mã phân tích cú pháp xung quanh nó hay không.
Chris Poortrell

Mã tôi đã cung cấp được cho là chỉ dành cho logic kinh doanh. Vấn đề với cách tiếp cận của bạn là bạn không bao giờ biết giá trị nào được lưu trữ trong Union tại thời điểm biên dịch. Nó có nghĩa là bạn sẽ phải sử dụng câu lệnh if hoặc switch bất cứ khi nào bạn truy cập vào đối tượng Union, vì những đối tượng đó không có chung một chức năng! Bạn sẽ sử dụng thêm các đối tượng wrapper trong mã của mình như thế nào? Ngoài ra, bạn có thể xây dựng các đối tượng chung trong thời gian chạy (chậm, nhưng có thể). Một lựa chọn dễ dàng khác là trong bài viết đã chỉnh sửa của tôi.
Jaroslav Jandek

Về cơ bản bạn không có kiểm tra kiểu thời gian biên dịch có ý nghĩa trong mã của mình ngay bây giờ - bạn cũng có thể thử các đối tượng động (kiểm tra kiểu động trong thời gian chạy).
Jaroslav Jandek

2

Đây là nỗ lực của tôi. Nó thực hiện kiểm tra thời gian của các kiểu biên dịch, sử dụng các ràng buộc kiểu chung.

class Union {
    public interface AllowedType<T> { };

    internal object val;

    internal System.Type type;
}

static class UnionEx {
    public static T As<U,T>(this U x) where U : Union, Union.AllowedType<T> {
        return x.type == typeof(T) ?(T)x.val : default(T);
    }

    public static void Set<U,T>(this U x, T newval) where U : Union, Union.AllowedType<T> {
        x.val = newval;
        x.type = typeof(T);
    }

    public static bool Is<U,T>(this U x) where U : Union, Union.AllowedType<T> {
        return x.type == typeof(T);
    }
}

class MyType : Union, Union.AllowedType<int>, Union.AllowedType<string> {}

class TestIt
{
    static void Main()
    {
        MyType bla = new MyType();
        bla.Set(234);
        System.Console.WriteLine(bla.As<MyType,int>());
        System.Console.WriteLine(bla.Is<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,int>());

        bla.Set("test");
        System.Console.WriteLine(bla.As<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,string>());
        System.Console.WriteLine(bla.Is<MyType,int>());

        // compile time errors!
        // bla.Set('a'); 
        // bla.Is<MyType,char>()
    }
}

Nó có thể sử dụng một số khá lên. Đặc biệt, tôi không thể tìm ra cách loại bỏ các tham số kiểu thành As / Is / Set (không có cách nào để chỉ định một tham số kiểu và để C # tìm ra tham số còn lại?)


2

Vì vậy, tôi đã gặp phải vấn đề tương tự này nhiều lần và tôi chỉ nghĩ ra một giải pháp có được cú pháp mà tôi muốn (với cái giá phải trả là sự xấu xí trong việc triển khai kiểu Union.)

Tóm lại: chúng tôi muốn loại sử dụng này tại trang web cuộc gọi.

Union<int, string> u;

u = 1492;
int yearColumbusDiscoveredAmerica = u;

u = "hello world";
string traditionalGreeting = u;

var answers = new SortedList<string, Union<int, string, DateTime>>();
answers["life, the universe, and everything"] = 42;
answers["D-Day"] = new DateTime(1944, 6, 6);
answers["C#"] = "is awesome";

Tuy nhiên, chúng tôi muốn các ví dụ sau không biên dịch được để chúng tôi nhận được một modicum của kiểu an toàn.

DateTime dateTimeColumbusDiscoveredAmerica = u;
Foo fooInstance = u;

Để có thêm tín dụng, chúng ta cũng đừng chiếm nhiều dung lượng hơn mức cần thiết.

Với tất cả những gì đã nói, đây là cách triển khai của tôi cho hai tham số kiểu chung. Việc triển khai cho các tham số kiểu ba, bốn, v.v. là dễ dàng.

public abstract class Union<T1, T2>
{
    public abstract int TypeSlot
    {
        get;
    }

    public virtual T1 AsT1()
    {
        throw new TypeAccessException(string.Format(
            "Cannot treat this instance as a {0} instance.", typeof(T1).Name));
    }

    public virtual T2 AsT2()
    {
        throw new TypeAccessException(string.Format(
            "Cannot treat this instance as a {0} instance.", typeof(T2).Name));
    }

    public static implicit operator Union<T1, T2>(T1 data)
    {
        return new FromT1(data);
    }

    public static implicit operator Union<T1, T2>(T2 data)
    {
        return new FromT2(data);
    }

    public static implicit operator Union<T1, T2>(Tuple<T1, T2> data)
    {
        return new FromTuple(data);
    }

    public static implicit operator T1(Union<T1, T2> source)
    {
        return source.AsT1();
    }

    public static implicit operator T2(Union<T1, T2> source)
    {
        return source.AsT2();
    }

    private class FromT1 : Union<T1, T2>
    {
        private readonly T1 data;

        public FromT1(T1 data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 1; } 
        }

        public override T1 AsT1()
        { 
            return this.data;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }

    private class FromT2 : Union<T1, T2>
    {
        private readonly T2 data;

        public FromT2(T2 data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 2; } 
        }

        public override T2 AsT2()
        { 
            return this.data;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }

    private class FromTuple : Union<T1, T2>
    {
        private readonly Tuple<T1, T2> data;

        public FromTuple(Tuple<T1, T2> data)
        {
            this.data = data;
        }

        public override int TypeSlot 
        { 
            get { return 0; } 
        }

        public override T1 AsT1()
        { 
            return this.data.Item1;
        }

        public override T2 AsT2()
        { 
            return this.data.Item2;
        }

        public override string ToString()
        {
            return this.data.ToString();
        }

        public override int GetHashCode()
        {
            return this.data.GetHashCode();
        }
    }
}

2

Và nỗ lực của tôi về giải pháp tối thiểu nhưng có thể mở rộng bằng cách sử dụng lồng loại Union / Either . Ngoài ra, việc sử dụng các tham số mặc định trong phương thức Đối sánh sẽ tự nhiên bật kịch bản "X hoặc Mặc định".

using System;
using System.Reflection;
using NUnit.Framework;

namespace Playground
{
    [TestFixture]
    public class EitherTests
    {
        [Test]
        public void Test_Either_of_Property_or_FieldInfo()
        {
            var some = new Some(false);
            var field = some.GetType().GetField("X");
            var property = some.GetType().GetProperty("Y");
            Assert.NotNull(field);
            Assert.NotNull(property);

            var info = Either<PropertyInfo, FieldInfo>.Of(field);
            var infoType = info.Match(p => p.PropertyType, f => f.FieldType);

            Assert.That(infoType, Is.EqualTo(typeof(bool)));
        }

        [Test]
        public void Either_of_three_cases_using_nesting()
        {
            var some = new Some(false);
            var field = some.GetType().GetField("X");
            var parameter = some.GetType().GetConstructors()[0].GetParameters()[0];
            Assert.NotNull(field);
            Assert.NotNull(parameter);

            var info = Either<ParameterInfo, Either<PropertyInfo, FieldInfo>>.Of(parameter);
            var name = info.Match(_ => _.Name, _ => _.Name, _ => _.Name);

            Assert.That(name, Is.EqualTo("a"));
        }

        public class Some
        {
            public bool X;
            public string Y { get; set; }

            public Some(bool a)
            {
                X = a;
            }
        }
    }

    public static class Either
    {
        public static T Match<A, B, C, T>(
            this Either<A, Either<B, C>> source,
            Func<A, T> a = null, Func<B, T> b = null, Func<C, T> c = null)
        {
            return source.Match(a, bc => bc.Match(b, c));
        }
    }

    public abstract class Either<A, B>
    {
        public static Either<A, B> Of(A a)
        {
            return new CaseA(a);
        }

        public static Either<A, B> Of(B b)
        {
            return new CaseB(b);
        }

        public abstract T Match<T>(Func<A, T> a = null, Func<B, T> b = null);

        private sealed class CaseA : Either<A, B>
        {
            private readonly A _item;
            public CaseA(A item) { _item = item; }

            public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
            {
                return a == null ? default(T) : a(_item);
            }
        }

        private sealed class CaseB : Either<A, B>
        {
            private readonly B _item;
            public CaseB(B item) { _item = item; }

            public override T Match<T>(Func<A, T> a = null, Func<B, T> b = null)
            {
                return b == null ? default(T) : b(_item);
            }
        }
    }
}

1

Bạn có thể ném các ngoại lệ khi có nỗ lực truy cập các biến chưa được khởi tạo, tức là nếu nó được tạo bằng tham số A và sau đó có nỗ lực truy cập B hoặc C, nó có thể ném ra, chẳng hạn như UnsupportedOperationException. Mặc dù vậy, bạn sẽ cần một bộ thu gọn để làm cho nó hoạt động.


Có - phiên bản đầu tiên mà tôi viết đã nêu ra ngoại lệ trong phương thức As - nhưng trong khi điều này chắc chắn làm nổi bật vấn đề trong mã, tôi thích được nói về điều này vào lúc biên dịch hơn là lúc chạy.
Chris Poortrell

0

Bạn có thể xuất một hàm đối sánh mẫu giả, giống như tôi sử dụng cho loại Either trong thư viện Sasa của tôi . Hiện tại có chi phí thời gian chạy, nhưng cuối cùng tôi dự định thêm một phân tích CIL để nội tuyến tất cả các đại biểu vào một tuyên bố trường hợp đúng.


0

Không thể thực hiện với chính xác cú pháp bạn đã sử dụng nhưng với độ dài dòng hơn một chút và sao chép / dán, thật dễ dàng để giải quyết quá tải thực hiện công việc cho bạn:


// this code is ok
var u = new Union("");
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

// and this one will not compile
if (u.Value(Is.OfType()))
{
    u.Value(Get.ForType());
}

Bây giờ nó đã khá rõ ràng làm thế nào để thực hiện nó:


    public class Union
    {
        private readonly Type type;
        public readonly A a;
        public readonly B b;
        public readonly C c;

        public Union(A a)
        {
            type = typeof(A);
            this.a = a;
        }

        public Union(B b)
        {
            type = typeof(B);
            this.b = b;
        }

        public Union(C c)
        {
            type = typeof(C);
            this.c = c;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(A) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(B) == type;
        }

        public bool Value(TypeTestSelector _)
        {
            return typeof(C) == type;
        }

        public A Value(GetValueTypeSelector _)
        {
            return a;
        }

        public B Value(GetValueTypeSelector _)
        {
            return b;
        }

        public C Value(GetValueTypeSelector _)
        {
            return c;
        }
    }

    public static class Is
    {
        public static TypeTestSelector OfType()
        {
            return null;
        }
    }

    public class TypeTestSelector
    {
    }

    public static class Get
    {
        public static GetValueTypeSelector ForType()
        {
            return null;
        }
    }

    public class GetValueTypeSelector
    {
    }

Không có kiểm tra nào để trích xuất giá trị của kiểu sai, ví dụ:


var u = Union(10);
string s = u.Value(Get.ForType());

Vì vậy, bạn có thể xem xét thêm các kiểm tra cần thiết và ném các ngoại lệ trong những trường hợp như vậy.


0

Tôi sử dụng riêng của Loại Liên minh.

Hãy xem xét một ví dụ để làm cho nó rõ ràng hơn.

Hãy tưởng tượng chúng ta có lớp Liên hệ:

public class Contact 
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string PostalAdrress { get; set; }
}

Tất cả đều được định nghĩa là chuỗi đơn giản, nhưng thực sự chúng chỉ là chuỗi? Dĩ nhiên là không. Tên có thể bao gồm Tên và Họ. Hay là một Email chỉ là một tập hợp các ký hiệu? Tôi biết rằng ít nhất nó nên chứa @ và nó nhất thiết phải có.

Hãy cải thiện mô hình miền của chúng tôi

public class PersonalName 
{
    public PersonalName(string firstName, string lastName) { ... }
    public string Name() { return _fistName + " " _lastName; }
}

public class EmailAddress 
{
    public EmailAddress(string email) { ... } 
}

public class PostalAdrress 
{
    public PostalAdrress(string address, string city, int zip) { ... } 
}

Trong các lớp này sẽ là các xác nhận trong quá trình tạo và cuối cùng chúng ta sẽ có các mô hình hợp lệ. Consturctor trong lớp PersonaName yêu cầu FirstName và LastName cùng một lúc. Điều này có nghĩa là sau khi tạo, nó không thể có trạng thái không hợp lệ.

Và lớp liên hệ tương ứng

public class Contact 
{
    public PersonalName Name { get; set; }
    public EmailAdress EmailAddress { get; set; }
    public PostalAddress PostalAddress { get; set; }
}

Trong trường hợp này, chúng ta có cùng một vấn đề, đối tượng của lớp Liên hệ có thể ở trạng thái không hợp lệ. Ý tôi là nó có thể có Địa chỉ Email nhưng không có Tên

var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };

Hãy sửa nó và tạo lớp Liên hệ với phương thức khởi tạo yêu cầu PersonalName, EmailAddress và PostalAddress:

public class Contact 
{
    public Contact(
               PersonalName personalName, 
               EmailAddress emailAddress,
               PostalAddress postalAddress
           ) 
    { 
         ... 
    }
}

Nhưng ở đây chúng ta có một vấn đề khác. Điều gì sẽ xảy ra nếu Người chỉ có Địa chỉ Email và không có Địa chỉ Bưu điện?

Nếu chúng ta nghĩ về nó ở đó, chúng ta nhận ra rằng có ba khả năng trạng thái hợp lệ của đối tượng lớp Liên hệ:

  1. Một liên hệ chỉ có một địa chỉ email
  2. Một liên hệ chỉ có một địa chỉ bưu điện
  3. Một liên hệ có cả địa chỉ email và địa chỉ bưu điện

Hãy viết ra các mô hình miền. Đầu tiên, chúng ta sẽ tạo lớp Thông tin liên hệ mà trạng thái sẽ tương ứng với các trường hợp trên.

public class ContactInfo 
{
    public ContactInfo(EmailAddress emailAddress) { ... }
    public ContactInfo(PostalAddress postalAddress) { ... }
    public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}

Và lớp Liên hệ:

public class Contact 
{
    public Contact(
              PersonalName personalName,
              ContactInfo contactInfo
           )
    {
        ...
    }
}

Hãy thử sử dụng nó:

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases

Hãy thêm phương thức Match trong lớp ContactInfo

public class ContactInfo 
{
   // constructor 
   public TResult Match<TResult>(
                      Func<EmailAddress,TResult> f1,
                      Func<PostalAddress,TResult> f2,
                      Func<Tuple<EmailAddress,PostalAddress>> f3
                  )
   {
        if (_emailAddress != null) 
        {
             return f1(_emailAddress);
        } 
        else if(_postalAddress != null)
        {
             ...
        } 
        ...
   }
}

Trong phương thức so khớp, chúng ta có thể viết mã này, bởi vì trạng thái của lớp liên hệ được điều khiển bằng các hàm tạo và nó có thể chỉ có một trong các trạng thái khả dĩ.

Hãy tạo một lớp bổ trợ, để mỗi lần không viết nhiều mã.

public abstract class Union<T1,T2,T3>
    where T1 : class
    where T2 : class
    where T3 : class
{
    private readonly T1 _t1;
    private readonly T2 _t2;
    private readonly T3 _t3;
    public Union(T1 t1) { _t1 = t1; }
    public Union(T2 t2) { _t2 = t2; }
    public Union(T3 t3) { _t3 = t3; }

    public TResult Match<TResult>(
            Func<T1, TResult> f1,
            Func<T2, TResult> f2,
            Func<T3, TResult> f3
        )
    {
        if (_t1 != null)
        {
            return f1(_t1);
        }
        else if (_t2 != null)
        {
            return f2(_t2);
        }
        else if (_t3 != null)
        {
            return f3(_t3);
        }
        throw new Exception("can't match");
    }
}

Chúng ta có thể có trước một lớp như vậy cho một số kiểu, như đã làm với Func, Action của ủy nhiệm. 4-6 tham số kiểu chung sẽ có đầy đủ cho lớp Union.

Hãy viết lại ContactInfolớp:

public sealed class ContactInfo : Union<
                                     EmailAddress,
                                     PostalAddress,
                                     Tuple<EmaiAddress,PostalAddress>
                                  >
{
    public Contact(EmailAddress emailAddress) : base(emailAddress) { }
    public Contact(PostalAddress postalAddress) : base(postalAddress) { }
    public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}

Ở đây trình biên dịch sẽ yêu cầu ghi đè cho ít nhất một hàm tạo. Nếu chúng ta quên ghi đè phần còn lại của các hàm tạo, chúng ta không thể tạo đối tượng của lớp ContactInfo với trạng thái khác. Điều này sẽ bảo vệ chúng tôi khỏi các ngoại lệ về thời gian chạy trong quá trình Đối sánh.

var contact = new Contact(
                  new PersonalName("James", "Bond"),
                  new ContactInfo(
                      new EmailAddress("agent@007.com")
                  )
               );
Console.WriteLine(contact.PersonalName()); // James Bond
Console
    .WriteLine(
        contact
            .ContactInfo()
            .Match(
                (emailAddress) => emailAddress.Address,
                (postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
                (emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
            )
    );

Đó là tất cả. Tôi mong la bạn thich.

Ví dụ được lấy từ trang F # cho niềm vui và lợi nhuận


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.