chuyển đổi / mô hình phù hợp với ý tưởng


151

Gần đây tôi đã xem xét F # và trong khi tôi không có khả năng sớm vượt qua hàng rào, nó chắc chắn làm nổi bật một số lĩnh vực mà C # (hoặc hỗ trợ thư viện) có thể giúp cuộc sống dễ dàng hơn.

Cụ thể, tôi đang suy nghĩ về khả năng khớp mẫu của F #, cho phép cú pháp rất phong phú - biểu cảm hơn nhiều so với tương đương C # chuyển đổi / điều kiện hiện tại. Tôi sẽ không thử đưa ra một ví dụ trực tiếp (F # của tôi không phù hợp với nó), nhưng tóm lại, nó cho phép:

  • khớp theo loại (với kiểm tra phạm vi bảo hiểm đầy đủ cho các hiệp hội bị phân biệt đối xử) [lưu ý điều này cũng tạo ra loại cho biến bị ràng buộc, cho phép truy cập thành viên, v.v.]
  • phù hợp với vị ngữ
  • kết hợp của những điều trên (và có thể một số tình huống khác mà tôi không biết)

Mặc dù thật đáng yêu khi C # cuối cùng đã mượn [ahem] một số sự phong phú này, nhưng trong thời gian tạm thời tôi đã xem xét những gì có thể được thực hiện trong thời gian chạy - ví dụ, khá dễ dàng để kết hợp một số đối tượng để cho phép:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

trong đó getRentprice là Func <Xe, int>.

[lưu ý - có thể Switch / Case ở đây là thuật ngữ sai ... nhưng nó cho thấy ý tưởng]

Đối với tôi, điều này rõ ràng hơn nhiều so với việc sử dụng lặp lại if / other hoặc điều kiện ternary hỗn hợp (điều này rất lộn xộn cho các biểu thức không tầm thường - ngoặc galore). Nó cũng tránh được nhiều lần truyền và cho phép mở rộng đơn giản (trực tiếp hoặc thông qua các phương thức mở rộng) cho các kết quả khớp cụ thể hơn, ví dụ: khớp InRange (...) có thể so sánh với VB Chọn ... Case "x To y " sử dụng.

Tôi chỉ đang cố gắng đánh giá xem mọi người có nghĩ rằng có nhiều lợi ích từ các cấu trúc như trên không (trong trường hợp không có hỗ trợ ngôn ngữ)?

Ngoài ra, xin lưu ý rằng tôi đã chơi với 3 biến thể ở trên:

  • một phiên bản Func <TSource, TValue> để đánh giá - có thể so sánh với các câu lệnh điều kiện ternary tổng hợp
  • phiên bản Hành động <TSource> - có thể so sánh với if / other if / other if / other if / other
  • một phiên bản <Func <TSource, TValue >> - là phiên bản đầu tiên, nhưng có thể sử dụng được bởi các nhà cung cấp LINQ tùy ý

Ngoài ra, sử dụng phiên bản dựa trên Biểu thức cho phép viết lại cây Biểu thức, về cơ bản nội tuyến tất cả các nhánh thành Biểu thức điều kiện tổng hợp duy nhất, thay vì sử dụng lệnh gọi lặp lại. Gần đây tôi đã không kiểm tra, nhưng trong một số bản dựng Entity Framework đầu tiên, tôi dường như nhớ lại điều này là cần thiết, vì nó không giống như InvocationExpression rất nhiều. Nó cũng cho phép sử dụng hiệu quả hơn với LINQ-to-Object, vì nó tránh được các lệnh ủy nhiệm lặp đi lặp lại - các thử nghiệm cho thấy một trận đấu giống như trên (sử dụng biểu thức Biểu thức) thực hiện ở cùng tốc độ [nhanh hơn một chút so với C # tương đương tuyên bố điều kiện tổng hợp. Để hoàn thiện, phiên bản dựa trên Func <...> mất 4 lần so với tuyên bố điều kiện C #, nhưng vẫn rất nhanh và không chắc là một nút cổ chai lớn trong hầu hết các trường hợp sử dụng.

Tôi hoan nghênh bất kỳ suy nghĩ / đầu vào / phê bình / vv nào ở trên (hoặc về khả năng hỗ trợ ngôn ngữ C # phong phú hơn ... đây là hy vọng ;-p).


"Tôi chỉ đang cố gắng đánh giá xem mọi người có nghĩ rằng có nhiều lợi ích từ các cấu trúc như trên không (trong trường hợp không có hỗ trợ ngôn ngữ)?" IMHO, vâng. Không phải cái gì đó tương tự đã tồn tại? Nếu không, cảm thấy được khuyến khích để viết một thư viện nhẹ.
Konrad Rudolph

10
Bạn có thể sử dụng VB .NET hỗ trợ điều này trong câu lệnh chọn trường hợp. Ôi!
Jim Burger

Tôi cũng sẽ tự mình bấm còi và thêm một liên kết đến thư viện của mình: dotnet chức năng
Alexey Romanov

1
Tôi thích ý tưởng này và nó làm cho một hình thức chuyển đổi rất đẹp và linh hoạt hơn nhiều; tuy nhiên, đây có thực sự là một cách sử dụng cú pháp giống như Linq như một trình bao bọc if-then không? Tôi sẽ không khuyến khích ai đó sử dụng điều này thay cho thỏa thuận thực sự, tức là một switch-casetuyên bố. Đừng hiểu sai ý tôi, tôi nghĩ nó có chỗ đứng và có lẽ tôi sẽ tìm cách để thực hiện.
I Ab.

2
Mặc dù câu hỏi này đã hơn hai năm tuổi, nhưng tôi cảm thấy thích hợp khi đề cập rằng C # 7 sẽ sớm ra mắt (ish) với khả năng khớp mẫu.
Abion47

Câu trả lời:


22

Tôi biết đó là một chủ đề cũ, nhưng trong c # 7 bạn có thể làm:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Sự khác biệt đáng chú ý ở đây giữa C # và F # là tính đầy đủ của khớp mẫu. Rằng mẫu phù hợp bao gồm mọi trường hợp có thể có, được mô tả đầy đủ, cảnh báo từ trình biên dịch nếu bạn không. Mặc dù bạn có thể lập luận một cách đúng đắn rằng trường hợp mặc định thực hiện điều này, nhưng nó cũng thường là một ngoại lệ trong thời gian chạy.
VoronoiPotato

37

Sau khi thử làm những thứ "chức năng" như vậy trong C # (và thậm chí thử một cuốn sách về nó), tôi đã đi đến kết luận rằng không, với một vài ngoại lệ, những thứ như vậy không giúp ích quá nhiều.

Lý do chính là các ngôn ngữ như F # nhận được rất nhiều sức mạnh từ việc thực sự hỗ trợ các tính năng này. Không phải "bạn có thể làm được", mà là "nó đơn giản, rõ ràng, nó được mong đợi".

Chẳng hạn, trong khớp mẫu, bạn nhận được trình biên dịch cho bạn biết nếu có một kết quả khớp không hoàn chỉnh hoặc khi một kết quả khớp khác sẽ không bao giờ bị đánh. Điều này ít hữu ích hơn với các loại kết thúc mở, nhưng khi khớp với một liên minh hoặc bộ dữ liệu bị phân biệt đối xử, nó rất tiện lợi. Trong F #, bạn mong đợi mọi người khớp mẫu và nó ngay lập tức có ý nghĩa.

"Vấn đề" là một khi bạn bắt đầu sử dụng một số khái niệm chức năng, việc muốn tiếp tục là điều tự nhiên. Tuy nhiên, tận dụng các bộ dữ liệu, hàm, ứng dụng phương thức một phần và currying, khớp mẫu, hàm lồng nhau, tổng quát, hỗ trợ đơn nguyên, v.v. trong C # trở nên rất xấu, rất nhanh. Thật thú vị, và một số người rất thông minh đã thực hiện một số điều rất tuyệt vời trong C #, nhưng thực sự sử dụng nó cảm thấy nặng nề.

Những gì tôi đã kết thúc bằng cách sử dụng thường xuyên (xuyên dự án) trong C #:

  • Các hàm tuần tự, thông qua các phương thức mở rộng cho IEnumerable. Những thứ như ForEach hoặc Process ("Áp dụng"? - thực hiện một hành động trên một mục chuỗi như được liệt kê) phù hợp vì cú pháp C # hỗ trợ tốt cho nó.
  • Tóm tắt các mẫu tuyên bố phổ biến. Các khối thử / bắt / cuối cùng phức tạp hoặc các khối mã liên quan (thường rất chung chung). Việc mở rộng LINQ-to-SQL cũng phù hợp ở đây.
  • Tuples, ở một mức độ nào đó.

** Nhưng hãy lưu ý: Việc thiếu khái quát hóa tự động và suy luận kiểu thực sự cản trở việc sử dụng ngay cả các tính năng này. **

Tất cả điều này đã nói, như một người khác đã đề cập, trong một nhóm nhỏ, với mục đích cụ thể, vâng, có lẽ họ có thể giúp đỡ nếu bạn bị mắc kẹt với C #. Nhưng theo kinh nghiệm của tôi, họ thường cảm thấy rắc rối hơn giá trị - YMMV.

Một số liên kết khác:


25

Có thể cho rằng lý do khiến C # không đơn giản để bật loại là vì nó chủ yếu là ngôn ngữ hướng đối tượng và cách 'chính xác' để thực hiện điều này theo thuật ngữ hướng đối tượng sẽ là xác định phương thức GetRentprice trên Xe và ghi đè nó trong các lớp dẫn xuất.

Điều đó nói rằng, tôi đã dành một chút thời gian để chơi với nhiều ngôn ngữ và các ngôn ngữ chức năng như F # và Haskell có loại khả năng này và tôi đã đi qua một số nơi mà nó sẽ hữu ích trước đây (ví dụ như khi bạn không viết các loại bạn cần bật để bạn không thể thực hiện phương thức ảo trên chúng) và đó là điều tôi hoan nghênh vào ngôn ngữ cùng với các hiệp hội bị phân biệt đối xử.

[Chỉnh sửa: Đã xóa phần về hiệu suất như Marc chỉ ra rằng nó có thể bị ngắn mạch]

Một vấn đề tiềm năng khác là vấn đề về khả năng sử dụng - rõ ràng từ cuộc gọi cuối cùng sẽ xảy ra nếu trận đấu không đáp ứng bất kỳ điều kiện nào, nhưng hành vi là gì nếu nó phù hợp với hai điều kiện trở lên? Nó có nên ném một ngoại lệ? Nó nên trả lại trận đấu đầu tiên hay trận đấu cuối cùng?

Một cách tôi có xu hướng sử dụng để giải quyết loại vấn đề này là sử dụng trường từ điển với loại là khóa và lambda làm giá trị, điều này khá ngắn gọn để xây dựng bằng cú pháp khởi tạo đối tượng; tuy nhiên, điều này chỉ chiếm loại cụ thể và không cho phép các biến vị ngữ bổ sung nên có thể không phù hợp với các trường hợp phức tạp hơn. [Ghi chú bên cạnh - nếu bạn nhìn vào đầu ra của trình biên dịch C #, nó thường chuyển đổi các câu lệnh chuyển đổi thành các bảng nhảy dựa trên từ điển, do đó dường như không có lý do chính đáng để nó không thể hỗ trợ chuyển đổi các loại]


1
Trên thực tế - phiên bản tôi có ngắn mạch ở cả phiên bản đại biểu và biểu thức. Phiên bản biểu thức biên dịch thành một điều kiện ghép; phiên bản ủy nhiệm chỉ đơn giản là một tập hợp các vị từ và func / hành động - một khi nó có một kết quả khớp thì nó dừng lại.
Marc Gravell

Thú vị - từ một cái nhìn khó hiểu, tôi cho rằng nó sẽ phải thực hiện ít nhất là kiểm tra cơ bản từng điều kiện vì nó trông giống như một chuỗi phương thức, nhưng bây giờ tôi nhận ra các phương thức thực sự xâu chuỗi một đối tượng để xây dựng nó để bạn có thể làm điều này. Tôi sẽ chỉnh sửa câu trả lời của mình để xóa câu nói đó.
Greg Beech

22

Tôi không nghĩ các loại thư viện này (hoạt động như phần mở rộng ngôn ngữ) có thể được chấp nhận rộng rãi, nhưng chúng rất thú vị khi chơi và có thể thực sự hữu ích cho các nhóm nhỏ làm việc trong các miền cụ thể, nơi điều này hữu ích. Chẳng hạn, nếu bạn đang viết hàng tấn 'quy tắc / logic kinh doanh' thực hiện các bài kiểm tra loại tùy ý như thế này và không có gì, tôi có thể thấy nó sẽ hữu ích như thế nào.

Tôi không biết liệu đây có phải là một tính năng ngôn ngữ C # không (có vẻ nghi ngờ, nhưng ai có thể nhìn thấy tương lai?).

Để tham khảo, F # tương ứng là khoảng:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

giả sử bạn đã xác định một hệ thống phân cấp lớp dọc theo dòng

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors

2
Cảm ơn phiên bản F #. Tôi đoán tôi thích cách F # xử lý việc này, nhưng tôi không chắc chắn rằng (nói chung) F # là lựa chọn đúng đắn vào lúc này, vì vậy tôi phải đi bộ giữa đó ...
Marc Gravell

13

Để trả lời câu hỏi của bạn, vâng tôi nghĩ rằng các cấu trúc cú pháp khớp mẫu là hữu ích. Tôi cho một người muốn xem hỗ trợ cú pháp trong C # cho nó.

Đây là cách triển khai của tôi về một lớp cung cấp (gần) cú pháp giống như bạn mô tả

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Đây là một số mã kiểm tra:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }

9

Khớp mẫu (như được mô tả ở đây ), mục đích của nó là giải cấu trúc các giá trị theo đặc tả kiểu của chúng. Tuy nhiên, khái niệm về một lớp (hoặc loại) trong C # không đồng ý với bạn.

Có một điểm sai với thiết kế ngôn ngữ đa mô hình, ngược lại, thật tuyệt khi có lambdas trong C #, và Haskell có thể làm những thứ bắt buộc để ví dụ IO. Nhưng nó không phải là một giải pháp rất thanh lịch, không phải trong thời trang Haskell.

Nhưng vì các ngôn ngữ lập trình thủ tục tuần tự có thể được hiểu theo thuật toán lambda và C # xảy ra rất phù hợp với các tham số của ngôn ngữ thủ tục tuần tự, nên nó phù hợp. Nhưng, lấy một cái gì đó từ bối cảnh chức năng thuần túy của Haskell, và sau đó đưa tính năng đó vào một ngôn ngữ không thuần túy, làm tốt điều đó, sẽ không đảm bảo kết quả tốt hơn.

Quan điểm của tôi là điều này, điều làm cho đánh dấu khớp mẫu được gắn với thiết kế ngôn ngữ và mô hình dữ liệu. Phải nói rằng, tôi không tin rằng khớp mẫu là một tính năng hữu ích của C # vì nó không giải quyết được các vấn đề C # điển hình cũng như không phù hợp với mô hình lập trình mệnh lệnh.


1
Có lẽ. Thật vậy, tôi sẽ đấu tranh để nghĩ ra một lập luận "kẻ giết người" đầy thuyết phục về lý do tại sao nó lại cần thiết (trái ngược với "có lẽ tốt đẹp trong một vài trường hợp cạnh tranh với chi phí làm cho ngôn ngữ trở nên phức tạp hơn").
Marc Gravell

5

IMHO cách OO để làm những việc như vậy là mẫu Khách truy cập. Các phương thức thành viên khách truy cập của bạn chỉ đơn giản đóng vai trò là cấu trúc trường hợp và bạn để ngôn ngữ tự xử lý công văn phù hợp mà không phải "nhìn trộm" các loại.


4

Mặc dù không phải là 'C-sharpey' để bật loại, tôi biết rằng cấu trúc đó sẽ khá hữu ích trong sử dụng chung - tôi có ít nhất một dự án cá nhân có thể sử dụng nó (mặc dù ATM có thể quản lý được). Có nhiều vấn đề về hiệu năng biên dịch, với cây biểu thức viết lại không?


Không phải nếu bạn lưu trữ đối tượng để sử dụng lại (phần lớn là cách các biểu thức lambda C # hoạt động, ngoại trừ trình biên dịch ẩn mã). Việc viết lại chắc chắn cải thiện hiệu suất được biên dịch - tuy nhiên, để sử dụng thường xuyên (chứ không phải LINQ-to-Something) tôi hy vọng phiên bản dành cho đại biểu có thể hữu ích hơn.
Marc Gravell

Cũng lưu ý - không nhất thiết phải là loại chuyển đổi - nó cũng có thể được sử dụng như một điều kiện tổng hợp (thậm chí thông qua LINQ) - nhưng không có x => Thử nghiệm lộn xộn? Kết quả1: (Test2? Kết quả2: (Test3? Kết quả 3: Kết quả4))
Marc Gravell

Rất vui được biết, mặc dù tôi có nghĩa là hiệu suất của quá trình biên dịch thực tế : csc.exe mất bao lâu - Tôi không đủ quen thuộc với C # để biết điều đó có thực sự là một vấn đề không, nhưng đó là một vấn đề lớn đối với C ++.
Simon Buchan

csc sẽ không chớp mắt về điều này - nó rất giống với cách thức hoạt động của LINQ và trình biên dịch C # 3.0 khá tốt về các phương pháp mở rộng / LINQ, v.v.
Marc Gravell

3

Tôi nghĩ rằng điều này có vẻ thực sự thú vị (+1), nhưng một điều cần cẩn thận: trình biên dịch C # khá tốt trong việc tối ưu hóa các câu lệnh chuyển đổi. Không chỉ dành cho ngắn mạch - bạn có được IL hoàn toàn khác nhau tùy thuộc vào số lượng trường hợp bạn gặp phải.

Ví dụ cụ thể của bạn thực hiện điều gì đó tôi thấy rất hữu ích - không có cú pháp tương đương với từng trường hợp, như (ví dụ) typeof(Motorcycle) không phải là hằng số.

Điều này trở nên thú vị hơn trong ứng dụng động - logic của bạn ở đây có thể dễ dàng điều khiển dữ liệu, mang lại khả năng thực thi kiểu 'công cụ quy tắc'.


0

Bạn có thể đạt được những gì bạn đang có sau khi sử dụng thư viện tôi đã viết, được gọi là OneOf

Ưu điểm lớn hơn switch(và ifexceptions as control flow) là nó là thời gian biên dịch an toàn - không có xử lý mặc định hoặc rơi qua

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Đó là trên Nuget và nhắm mục tiêu net451 và netst Chuẩn1.6

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.