2008-09-15 22 views
25

Có cách nào khả thi để sử dụng Generics để tạo một thư viện Math không phụ thuộc vào loại cơ sở được chọn để lưu trữ dữ liệu?Tạo thư viện Toán bằng Generics trong C#

Nói cách khác, giả sử tôi muốn viết một lớp Phân số. Phân số có thể được biểu diễn bằng hai số nguyên hoặc hai hoặc không có gì. Điều quan trọng là bốn thao tác số học cơ bản được xác định rõ. Vì vậy, tôi muốn có thể viết Fraction<int> frac = new Fraction<int>(1,2) và/hoặc Fraction<double> frac = new Fraction<double>(0.1, 1.0).

Rất tiếc, không có giao diện đại diện cho bốn thao tác cơ bản (+, -, *, /). Có ai tìm thấy một cách khả thi, khả thi để thực hiện điều này?

Trả lời

25

Dưới đây là một cách để trừu tượng ra các nhà khai thác đó là khá dễ dàng.

abstract class MathProvider<T> 
    { 
     public abstract T Divide(T a, T b); 
     public abstract T Multiply(T a, T b); 
     public abstract T Add(T a, T b); 
     public abstract T Negate(T a); 
     public virtual T Subtract(T a, T b) 
     { 
      return Add(a, Negate(b)); 
     } 
    } 

    class DoubleMathProvider : MathProvider<double> 
    { 
     public override double Divide(double a, double b) 
     { 
      return a/b; 
     } 

     public override double Multiply(double a, double b) 
     { 
      return a * b; 
     } 

     public override double Add(double a, double b) 
     { 
      return a + b; 
     } 

     public override double Negate(double a) 
     { 
      return -a; 
     } 
    } 

    class IntMathProvider : MathProvider<int> 
    { 
     public override int Divide(int a, int b) 
     { 
      return a/b; 
     } 

     public override int Multiply(int a, int b) 
     { 
      return a * b; 
     } 

     public override int Add(int a, int b) 
     { 
      return a + b; 
     } 

     public override int Negate(int a) 
     { 
      return -a; 
     } 
    } 

    class Fraction<T> 
    { 
     static MathProvider<T> _math; 
     // Notice this is a type constructor. It gets run the first time a 
     // variable of a specific type is declared for use. 
     // Having _math static reduces overhead. 
     static Fraction() 
     { 
      // This part of the code might be cleaner by once 
      // using reflection and finding all the implementors of 
      // MathProvider and assigning the instance by the one that 
      // matches T. 
      if (typeof(T) == typeof(double)) 
       _math = new DoubleMathProvider() as MathProvider<T>; 
      else if (typeof(T) == typeof(int)) 
       _math = new IntMathProvider() as MathProvider<T>; 
      // ... assign other options here. 

      if (_math == null) 
       throw new InvalidOperationException(
        "Type " + typeof(T).ToString() + " is not supported by Fraction."); 
     } 

     // Immutable impementations are better. 
     public T Numerator { get; private set; } 
     public T Denominator { get; private set; } 

     public Fraction(T numerator, T denominator) 
     { 
      // We would want this to be reduced to simpilest terms. 
      // For that we would need GCD, abs, and remainder operations 
      // defined for each math provider. 
      Numerator = numerator; 
      Denominator = denominator; 
     } 

     public static Fraction<T> operator +(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Add(
        _math.Multiply(a.Numerator, b.Denominator), 
        _math.Multiply(b.Numerator, a.Denominator)), 
       _math.Multiply(a.Denominator, b.Denominator)); 
     } 

     public static Fraction<T> operator -(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Subtract(
        _math.Multiply(a.Numerator, b.Denominator), 
        _math.Multiply(b.Numerator, a.Denominator)), 
       _math.Multiply(a.Denominator, b.Denominator)); 
     } 

     public static Fraction<T> operator /(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Multiply(a.Numerator, b.Denominator), 
       _math.Multiply(a.Denominator, b.Numerator)); 
     } 

     // ... other operators would follow. 
    } 

Nếu bạn không triển khai loại bạn sử dụng, bạn sẽ gặp lỗi khi chạy thay vì thời gian biên dịch (điều đó là xấu). Định nghĩa của việc triển khai MathProvider<T> luôn luôn giống nhau (cũng xấu). Tôi sẽ đề nghị bạn chỉ nên tránh làm điều này trong C# và sử dụng F # hoặc một số ngôn ngữ khác phù hợp hơn với mức trừu tượng này.

Chỉnh sửa: Định nghĩa cộng và trừ cho Fraction<T>. Một điều thú vị và đơn giản khác cần làm là triển khai MathProvider hoạt động trên một cây cú pháp trừu tượng. Ý tưởng này ngay lập tức chỉ để làm những việc như sự khác biệt tự động: http://conal.net/papers/beautiful-differentiation/

+0

đẹp một sạch :) –

+1

Nói chung, tôi nghĩ rằng MathProvider nên được thực hiện vào một giao diện và Trừ vào một phương pháp giao diện thông thường hoặc nó có thể được thực hiện như là một phương pháp mở rộng. Mặt khác, điều đó sẽ không thừa nhận nó. – dalle

+0

Tôi tự hỏi về hiệu suất của giải pháp của bạn ... Chỉ hoạt động tốt nếu mọi thứ được inline ... –

1

Đầu tiên, lớp của bạn nên giới hạn tham số chung cho nguyên thủy (phân lớp công khai trong đó T: struct, new()).

Thứ hai, bạn có thể cần phải tạo implicit cast overloads để bạn có thể xử lý truyền từ loại này sang loại khác mà không cần trình biên dịch khóc.

Thứ ba, bạn có thể quá tải bốn toán tử cơ bản để làm cho giao diện linh hoạt hơn khi kết hợp các phân số của các loại khác nhau.

Cuối cùng, bạn phải xem xét cách bạn đang xử lý số học trên và dưới. Một thư viện tốt sẽ cực kỳ rõ ràng trong cách nó xử lý tràn; nếu không bạn không thể tin tưởng vào kết quả hoạt động của các loại phân số khác nhau.

+1

Vấn đề là tôi thậm chí không thể thực hiện các khoản tiền như vậy bởi vì cấu trúc không có toán tử bổ sung được xác định. – Sklivvz

+0

http://msdn.microsoft.com/en-us/library/aa691324 (VS.71).aspx "các triển khai do người dùng định nghĩa có thể được giới thiệu bằng cách bao gồm các khai báo toán tử trong các lớp và các cấu trúc" – Will

6

Tôi tin rằng đây trả lời câu hỏi của bạn:

http://www.codeproject.com/KB/cs/genericnumerics.aspx

+1

Giải pháp đó, và các giải pháp khác (như sử dụng Emit) không thực sự sạch sẽ, vì vậy đó không phải là những gì tôi đang tìm kiếm. Nhưng cảm ơn anyways :) – Sklivvz

2

Đây là một vấn đề tinh tế đi kèm với các loại chung chung. Giả sử một thuật toán liên quan đến sự phân chia, giả sử loại bỏ Gaussian để giải quyết một hệ phương trình. Nếu bạn chuyển số nguyên, bạn sẽ nhận được câu trả lời sai bởi vì bạn sẽ thực hiện phân chia số nguyên. Nhưng nếu bạn vượt qua hai đối số xảy ra có giá trị số nguyên, bạn sẽ nhận được câu trả lời đúng.

Điều tương tự cũng xảy ra với căn bậc hai, như trong hệ số Cholesky. Việc tính toán một ma trận số nguyên sẽ bị sai, trong khi việc tính toán một ma trận của các cặp đôi xảy ra có các giá trị số nguyên sẽ được sử dụng tốt.