2010-05-23 27 views
18

Tôi đang làm việc để viết lại giao diện thông thạo của mình cho thư viện lớp IoC, và khi tôi tái cấu trúc một số mã để chia sẻ một số chức năng phổ biến thông qua một lớp cơ sở, tôi nhấn vào một snag.Suy luận kiểu chung một phần có thể có trong C#?

Note: Đây là điều mà tôi muốn để làm, không phải cái gì tôi để làm. Nếu tôi phải làm với một cú pháp khác, tôi sẽ, nhưng nếu có ai có ý tưởng về cách làm cho mã của tôi biên dịch theo cách tôi muốn, nó sẽ được chào đón nhiều nhất.

Tôi muốn một số phương thức mở rộng có sẵn cho một lớp cơ sở cụ thể và các phương thức này phải chung chung, với một kiểu chung, liên quan đến một đối số cho phương thức, nhưng các phương pháp cũng phải trả về một kiểu cụ thể liên quan đến con cháu cụ thể mà chúng được gọi.

Tốt hơn với ví dụ mã so với mô tả ở trên.

Dưới đây là một ví dụ đơn giản và đầy đủ về những gì không công việc:

using System; 

namespace ConsoleApplication16 
{ 
    public class ParameterizedRegistrationBase { } 
    public class ConcreteTypeRegistration : ParameterizedRegistrationBase 
    { 
     public void SomethingConcrete() { } 
    } 
    public class DelegateRegistration : ParameterizedRegistrationBase 
    { 
     public void SomethingDelegated() { } 
    } 

    public static class Extensions 
    { 
     public static ParameterizedRegistrationBase Parameter<T>(
      this ParameterizedRegistrationBase p, string name, T value) 
     { 
      return p; 
     } 
    } 

    class Program 
    { 
     static void Main(string[] args) 
     { 
      ConcreteTypeRegistration ct = new ConcreteTypeRegistration(); 
      ct 
       .Parameter<int>("age", 20) 
       .SomethingConcrete(); // <-- this is not available 

      DelegateRegistration del = new DelegateRegistration(); 
      del 
       .Parameter<int>("age", 20) 
       .SomethingDelegated(); // <-- neither is this 
     } 
    } 
} 

Nếu bạn biên dịch này, bạn sẽ nhận được:

'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingConcrete' and no extension method 'SomethingConcrete'... 
'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingDelegated' and no extension method 'SomethingDelegated'... 

Những gì tôi muốn là dành cho phần mở rộng phương pháp (Parameter<T>) để có thể được gọi trên cả hai ConcreteTypeRegistrationDelegateRegistration và trong cả hai trường hợp, loại trả về phải khớp với loại tiện ích được gọi.

Vấn đề là như sau:

Tôi muốn viết:

ct.Parameter<string>("name", "Lasse") 
      ^------^ 
      notice only one generic argument 

mà còn Parameter<T> trả về một đối tượng cùng loại nó đã được gọi về, có nghĩa là:

ct.Parameter<string>("name", "Lasse").SomethingConcrete(); 
^          ^-------+-------^ 
|            | 
+---------------------------------------------+ 
    .SomethingConcrete comes from the object in "ct" 
    which in this case is of type ConcreteTypeRegistration 

Có cách nào tôi có thể lừa trình biên dịch tạo ra bước nhảy vọt này cho tôi không?

Nếu tôi thêm hai đối số kiểu chung chung cho phương thức Parameter, suy luận kiểu buộc tôi để thể cung cấp cả hai, hoặc không, có nghĩa là điều này:

public static TReg Parameter<TReg, T>(
    this TReg p, string name, T value) 
    where TReg : ParameterizedRegistrationBase 

mang lại cho tôi điều này:

Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments 
Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments 

Mà chỉ là xấu.

Tôi có thể dễ dàng sắp xếp lại các lớp, hoặc thậm chí làm cho các phương thức không mở rộng phương pháp bằng cách giới thiệu chúng vào hệ thống phân cấp, nhưng câu hỏi của tôi là nếu tôi có thể tránh phải sao chép các phương thức cho hai hậu duệ. chỉ khai báo chúng một lần cho lớp cơ sở.

Hãy để tôi nói lại điều đó. Có cách nào để thay đổi các lớp trong ví dụ mã đầu tiên ở trên, sao cho cú pháp trong phương pháp chính có thể được giữ lại mà không sao chép các phương thức được đề cập?

Mã sẽ phải tương thích với cả C# 3.0 và 4.0.


Sửa: Lý do tôi không muốn rời cả hai đối số kiểu chung chung để suy luận là đối với một số dịch vụ, tôi muốn chỉ định một giá trị tham số cho một tham số nhà xây dựng mà là của một loại chuyền, tuy nhiên trong một giá trị là hậu duệ. Đối với thời điểm này, việc kết hợp các giá trị đối số được chỉ định và hàm tạo chính xác để gọi được thực hiện bằng cả tên và loại đối số.

Hãy để tôi đưa ra một ví dụ:

ServiceContainerBuilder.Register<ISomeService>(r => r 
    .From(f => f.ConcreteType<FileService>(ct => ct 
     .Parameter<Stream>("source", new FileStream(...))))); 
        ^--+---^    ^---+----^ 
        |      | 
        |      +- has to be a descendant of Stream 
        | 
        +- has to match constructor of FileService 

Nếu tôi rời cả hai để gõ suy luận, loại thông số sẽ FileStream, không Stream.

Trả lời

11

Nếu bạn chỉ có hai loại cụ thể đăng ký (mà dường như là trường hợp trong câu hỏi của bạn), bạn chỉ có thể thực hiện hai phương pháp khuyến nông:

public static DelegateRegistration Parameter<T>( 
    this DelegateRegistration p, string name, T value); 

public static ConcreteTypeRegistration Parameter<T>( 
    this ConcreteTypeRegistration p, string name, T value); 

Sau đó, bạn sẽ không cần phải xác định loại đối số, do đó suy luận kiểu sẽ hoạt động trong ví dụ bạn đã đề cập. Lưu ý rằng bạn có thể triển khai cả hai phương thức mở rộng chỉ bằng cách ủy nhiệm cho một phương thức mở rộng chung duy nhất với hai tham số kiểu (một tham số trong câu hỏi của bạn).


Nói chung, C# không hỗ trợ bất cứ điều gì như o.Foo<int, ?>(..) để suy ra chỉ tham số Loại thứ hai (nó sẽ là tính năng thú vị - F # có nó và nó :-) khá hữu ích). Bạn có thể có thể thực hiện một cách giải quyết mà sẽ cho phép bạn để viết những dòng này (về cơ bản, bằng cách tách biệt cuộc gọi vào hai cuộc gọi phương pháp, để có được hai nơi loại inferrence có thể được áp dụng):

FooTrick<int>().Apply(); // where Apply is a generic method 

Đây là một pseudo- mã để chứng minh cấu trúc:

// in the original object 
FooImmediateWrapper<T> FooTrick<T>() { 
    return new FooImmediateWrapper<T> { InvokeOn = this; } 
} 
// in the FooImmediateWrapper<T> class 
(...) Apply<R>(arguments) { 
    this.InvokeOn.Foo<T, R>(arguments); 
} 
+0

Vâng, trông giống như giải pháp tốt nhất. Tôi đang thử nghiệm một trong những nơi mà tôi đã giới thiệu generics vào lớp cơ sở là tốt, và chỉ cần thả phương pháp mở rộng, nhưng ví dụ của bạn cho phép tôi giữ chúng như là phương pháp mở rộng, mà tôi ủng hộ trong trường hợp này. Tức là, hai phương thức mở rộng chỉ gọi một phương thức phổ biến là chỉ định đúng loại. –

2

Tại sao bạn không chỉ định thông số loại 0? Cả hai có thể được suy ra trong mẫu của bạn. Nếu đây không phải là giải pháp có thể chấp nhận được cho bạn, tôi cũng thường xuyên gặp sự cố này và không có cách nào dễ dàng để giải quyết vấn đề "chỉ suy luận một tham số loại". Vì vậy, tôi sẽ đi với các phương pháp trùng lặp.

+0

Tôi muốn tránh điều đó, khi đăng ký loại bê tông vào triển khai IoC, tôi có thể cung cấp giá trị tham số cho tham số hàm tạo và tôi phải chỉ định tên tham số và loại giá trị để vượt qua nó. Loại được chỉ định phải khớp, nhưng giá trị thực được truyền có thể là hậu duệ. Ví dụ: '.Parameter (" nguồn ", FileStream mới (...))'. Ở đây, nếu tôi để cả hai suy luận, kiểu tham số sẽ là FileStream, và do đó tôi phải thực hiện phù hợp với con cháu. Tôi có thể làm điều đó mặc dù, tôi chỉ muốn không. –

0

gì về những điều sau:

Sử dụng định nghĩa mà bạn cung cấp: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase

Sau đó đúc các tham số để động cơ suy luận được đúng loại:

ServiceContainerBuilder.Register<ISomeService>(r => r 
.From(f => f.ConcreteType<FileService>(ct => ct 
    .Parameter("source", (Stream)new FileStream(...))))); 
0

Tôi nghĩ rằng bạn cần phải tách các thông số hai loại giữa hai biểu thức khác nhau; làm cho một phần rõ ràng là một phần của loại tham số cho phương thức mở rộng, do đó suy luận có thể lấy nó lên.

Giả sử bạn đã khai báo một lớp wrapper:

public class TypedValue<TValue> 
{ 
    public TypedValue(TValue value) 
    { 
     Value = value; 
    } 

    public TValue Value { get; private set; } 
} 

Sau đó, phương pháp khuyến nông của bạn như:

public static class Extensions 
{ 
    public static TReg Parameter<TValue, TReg>(
     this TReg p, string name, TypedValue<TValue> value) 
     where TReg : ParameterizedRegistrationBase 
    { 
     // can get at value.Value 
     return p; 
    } 
} 

Cộng với một tình trạng quá tải đơn giản hơn (ở trên có thể trên thực tế gọi này):

public static class Extensions 
{ 
    public static TReg Parameter<TValue, TReg>(
     this TReg p, string name, TValue value) 
     where TReg : ParameterizedRegistrationBase 
    { 
     return p; 
    } 
} 

Bây giờ trong trường hợp đơn giản, tại đó bạn vui lòng suy ra loại giá trị thông số:

ct.Parameter("name", "Lasse") 

Nhưng trong trường hợp bạn cần phải nêu rõ kiểu dữ liệu, bạn có thể làm như vậy:

ct.Parameter("list", new TypedValue<IEnumerable<int>>(new List<int>())) 

Trông xấu xí, nhưng hy vọng hiếm hơn so với loại hoàn toàn suy ra đơn giản.

Lưu ý rằng bạn có thể chỉ có tình trạng quá tải không-wrapper và viết:

ct.Parameter("list", (IEnumerable<int>)(new List<int>())) 

Nhưng điều đó tất nhiên có những bất lợi của thất bại trong thời gian chạy nếu bạn nhận được một cái gì đó sai. Thật không may, đi từ trình biên dịch C# của tôi ngay bây giờ, vì vậy xin lỗi nếu đây là cách tắt.

13

Tôi muốn tạo phương thức tiện ích có thể liệt kê trong danh sách các thứ và trả về danh sách những thứ thuộc một loại nhất định. Nó sẽ giống như sau:

listOfFruits.ThatAre<Banana>().Where(banana => banana.Peel != Color.Black) ... 

Đáng buồn thay, điều này là không thể. Chữ ký đề nghị phương pháp mở rộng này sẽ trông giống như:

public static IEnumerable<TResult> ThatAre<TSource, TResult> 
    (this IEnumerable<TSource> source) where TResult : TSource 

... và cuộc gọi đến ThatAre <> thất bại vì cả hai đối số loại cần phải được chỉ định, mặc dù TSource có thể được suy ra từ việc sử dụng.

Theo lời khuyên trong câu trả lời khác, tôi tạo ra hai chức năng: một mà nắm bắt được nguồn, và một người khác mà cho phép người gọi để thể hiện kết quả:

public static ThatAreWrapper<TSource> That<TSource> 
    (this IEnumerable<TSource> source) 
{ 
    return new ThatAreWrapper<TSource>(source); 
} 

public class ThatAreWrapper<TSource> 
{ 
    private readonly IEnumerable<TSource> SourceCollection; 
    public ThatAreWrapper(IEnumerable<TSource> source) 
    { 
     SourceCollection = source; 
    } 
    public IEnumerable<TResult> Are<TResult>() where TResult : TSource 
    { 
     foreach (var sourceItem in SourceCollection) 
      if (sourceItem is TResult) yield return (TResult)sourceItem; 
     } 
    } 
} 

Điều này dẫn đến mã gọi sau đây:

listOfFruits.That().Are<Banana>().Where(banana => banana.Peel != Color.Black) ... 

... không tệ.

Chú ý rằng vì những ràng buộc kiểu generic, đoạn mã sau:

listOfFruits.That().Are<Truck>().Where(truck => truck.Horn.IsBroken) ... 

sẽ thất bại trong việc biên soạn ở bước Are(), vì xe tải không phải là trái cây. Đây nhịp đập cung cấp .OfType <> function:

listOfFruits.OfType<Truck>().Where(truck => truck.Horn.IsBroken) ... 

này biên dịch, nhưng luôn mang lại không có kết quả và thực sự không có ý nghĩa gì để thử. Thật tuyệt vời khi cho phép trình biên dịch giúp bạn phát hiện ra những điều này.

+2

Tôi nghĩ rằng bạn đã nói điều tương tự như Thomas ở trên, nhưng tôi hiểu câu trả lời của bạn tốt hơn nhiều. –

+1

Vì tò mò, tại sao bạn không sử dụng '.OfType ()'? – recursive

+2

@recursive: vì .OfType không thực thi hạn chế mà loại bạn đang cố truyền sang phải tương thích với nguồn. Đã chỉnh sửa câu trả lời, đoạn cuối cùng. – CSJ

Các vấn đề liên quan