Bạn có nên khai báo các phương thức sử dụng quá tải hoặc các tham số tùy chọn trong C # 4.0 không?


94

Tôi đã xem bài nói chuyện của Anders về C # 4.0 và lén xem trước C # 5.0 , và điều đó khiến tôi suy nghĩ về việc khi các tham số tùy chọn có sẵn trong C # thì đâu sẽ là cách được khuyến nghị để khai báo các phương thức không cần tất cả các tham số được chỉ định?

Ví dụ, một cái gì đó giống như FileStreamlớp có khoảng mười lăm hàm tạo khác nhau có thể được chia thành các 'họ' hợp lý, ví dụ: những cái bên dưới từ một chuỗi, những cái từ an IntPtrvà những cái từ a SafeFileHandle.

FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

Đối với tôi, có vẻ như kiểu mẫu này có thể được đơn giản hóa bằng cách có ba hàm tạo thay thế và sử dụng các tham số tùy chọn cho những hàm có thể được mặc định, điều này sẽ làm cho các họ khác nhau của các hàm tạo khác biệt hơn [lưu ý: Tôi biết thay đổi này sẽ không được thực hiện trong BCL, tôi đang nói về giả thuyết cho loại tình huống này].

Bạn nghĩ sao? Từ C # 4.0 sẽ có ý nghĩa hơn khi biến các nhóm hàm tạo và phương thức liên quan chặt chẽ thành một phương thức duy nhất với các tham số tùy chọn, hay có lý do chính đáng để gắn bó với cơ chế nhiều quá tải truyền thống?

Câu trả lời:


122

Tôi sẽ xem xét những điều sau:

  • Bạn có cần mã của mình được sử dụng từ các ngôn ngữ không hỗ trợ các tham số tùy chọn không? Nếu vậy, hãy xem xét bao gồm các quá tải.
  • Bạn có thành viên nào trong nhóm phản đối dữ dội các thông số tùy chọn không? (Đôi khi sống với một quyết định mà bạn không thích sẽ dễ dàng hơn là tranh luận về trường hợp đó.)
  • Bạn có tự tin rằng các giá trị mặc định của bạn sẽ không thay đổi giữa các lần xây dựng mã của bạn hay nếu có, liệu những người gọi của bạn có đồng ý với điều đó không?

Tôi chưa kiểm tra xem các giá trị mặc định sẽ hoạt động như thế nào, nhưng tôi cho rằng các giá trị mặc định sẽ được đưa vào mã gọi, giống như các tham chiếu đến constcác trường. Điều đó thường không sao - những thay đổi đối với giá trị mặc định dù sao cũng khá quan trọng - nhưng đó là những điều cần xem xét.


21
+1 cho sự khôn ngoan về chủ nghĩa thực dụng: Đôi khi sống với một quyết định mà bạn không thích sẽ dễ dàng hơn là tranh luận về trường hợp đó.
Legends2k

13
@romkyns: Không, ảnh hưởng của quá tải không giống với điểm 3. Với quá tải cung cấp giá trị mặc định, các giá trị mặc định nằm trong mã thư viện - vì vậy nếu bạn thay đổi mặc định và cung cấp phiên bản mới của thư viện, người gọi sẽ xem mặc định mới mà không cần biên dịch lại. Trong khi với các tham số tùy chọn, bạn cần phải biên dịch lại để "xem" các giá trị mặc định mới. Rất nhiều lúc đó không phải là một sự khác biệt quan trọng, nhưng nó một sự khác biệt.
Jon Skeet

chào @JonSkeet, tôi muốn biết nếu chúng tôi sử dụng cả hai hàm ie với paramater tùy chọn và hàm khác với quá tải thì phương thức nào sẽ được gọi ?? ví dụ Add ​​(int a, int b) và Add (int a, int b, int c = 0) và gọi hàm nói: Add (5,10); phương thức nào sẽ được gọi là hàm nạp chồng hoặc hàm tham số tùy chọn? cảm ơn :)
SHEKHAR SHETE 22/09/2016

@Shekshar: Bạn đã thử chưa? Đọc thông số kỹ thuật để biết chi tiết, nhưng về cơ bản trong tie-breaker, một phương pháp mà trình biên dịch không phải điền bất kỳ thông số tùy chọn nào sẽ thắng.
Jon Skeet

@JonSkeet vừa rồi tôi đã thử với ở trên ... quá tải hàm thắng tham số tùy chọn :)
SHEKHAR SHETE 22/09/2016

19

Khi một phương thức quá tải thông thường thực hiện cùng một việc với một số đối số khác thì các giá trị mặc định sẽ được sử dụng.

Khi quá tải phương thức thực hiện một chức năng khác nhau dựa trên các tham số của nó thì quá trình nạp chồng sẽ tiếp tục được sử dụng.

Tôi đã sử dụng tùy chọn trở lại trong những ngày VB6 của mình và vì đã bỏ lỡ nó, nó sẽ giảm rất nhiều trùng lặp nhận xét XML trong C #.


11

Tôi đã sử dụng Delphi với các tham số tùy chọn mãi mãi. Thay vào đó, tôi đã chuyển sang sử dụng quá tải.

Bởi vì khi bạn tạo thêm quá tải, bạn sẽ luôn xung đột với một biểu mẫu tham số tùy chọn, và sau đó bạn sẽ phải chuyển đổi chúng thành không tùy chọn.

Và tôi thích khái niệm rằng nói chung có một phương pháp siêu việt , và phần còn lại là các trình bao bọc đơn giản hơn xung quanh phương pháp đó.


1
Tôi rất đồng ý với điều này, tuy nhiên, có một lưu ý rằng khi bạn có một phương thức có nhiều (3+) tham số, về bản chất tất cả đều là "tùy chọn" (có thể được thay thế bằng một mặc định), bạn có thể kết thúc bằng nhiều hoán vị của chữ ký phương thức không mang lại nhiều lợi ích. Xem xét Foo(A, B, C)đòi hỏi Foo(A), Foo(B), Foo(C), Foo(A, B), Foo(A, C), Foo(B, C).
Dan Lugg

7

Tôi chắc chắn sẽ sử dụng tính năng tham số tùy chọn của 4.0. Nó loại bỏ sự lố bịch ...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

... và đặt các giá trị ở ngay nơi người gọi có thể nhìn thấy chúng ...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

Đơn giản hơn nhiều và ít bị lỗi hơn nhiều. Tôi thực sự thấy đây là một lỗi trong trường hợp quá tải ...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

Tôi chưa chơi với trình biên dịch 4.0, nhưng tôi sẽ không bị sốc khi biết rằng trình biên dịch chỉ tạo ra quá tải cho bạn.


6

Các tham số tùy chọn về cơ bản là một đoạn siêu dữ liệu chỉ đạo trình biên dịch đang xử lý lệnh gọi phương thức để chèn các giá trị mặc định thích hợp tại trang web cuộc gọi. Ngược lại, quá tải cung cấp một phương tiện mà trình biên dịch có thể chọn một trong số các phương thức, một số phương thức có thể tự cung cấp các giá trị mặc định. Lưu ý rằng nếu một người cố gắng gọi một phương thức chỉ định các tham số tùy chọn từ mã được viết bằng ngôn ngữ không hỗ trợ chúng, trình biên dịch sẽ yêu cầu chỉ định các tham số "tùy chọn", nhưng vì gọi một phương thức mà không chỉ định tham số tùy chọn là tương đương với việc gọi nó với một tham số bằng giá trị mặc định, không có gì trở ngại đối với các ngôn ngữ gọi các phương thức như vậy.

Một hệ quả quan trọng của việc ràng buộc các tham số tùy chọn tại địa chỉ cuộc gọi là chúng sẽ được gán giá trị dựa trên phiên bản mã đích có sẵn cho trình biên dịch. Nếu một hợp ngữ Foocó một phương thức Boo(int)với giá trị mặc định là 5 và hợp ngữ Barchứa lệnh gọi đến Foo.Boo(), trình biên dịch sẽ xử lý điều đó dưới dạng a Foo.Boo(5). Nếu giá trị mặc định được thay đổi thành 6 và assembly Foođược biên dịch lại, Barsẽ tiếp tục gọi Foo.Boo(5)trừ khi hoặc cho đến khi nó được biên dịch lại với phiên bản mới của Foo. Do đó, người ta nên tránh sử dụng các tham số tùy chọn cho những thứ có thể thay đổi.


Re: "Do đó, người ta nên tránh sử dụng các tham số tùy chọn cho những thứ có thể thay đổi." Tôi đồng ý rằng điều này có thể có vấn đề nếu mã khách hàng không nhận thấy thay đổi. Tuy nhiên, cùng một vấn đề tồn tại khi giá trị mặc định được ẩn bên trong một tình trạng quá tải phương pháp: void Foo(int value) … void Foo() { Foo(42); }. Từ bên ngoài, người gọi không biết giá trị mặc định nào được sử dụng, cũng như khi nào nó có thể thay đổi; người ta sẽ phải giám sát tài liệu bằng văn bản cho điều đó. Giá trị mặc định cho các tham số tùy chọn có thể được xem như sau: tài liệu-trong mã giá trị mặc định là gì.
stakx - không còn đóng góp vào

@stakx: Nếu quá tải không tham số chuỗi thành quá tải có tham số, việc thay đổi giá trị "mặc định" của tham số đó và biên dịch lại định nghĩa của quá tải sẽ thay đổi giá trị mà nó sử dụng ngay cả khi mã gọi không được biên dịch lại .
supercat

Đúng nhưng điều đó không làm cho nó trở nên rắc rối hơn so với giải pháp thay thế. Trong một trường hợp (quá tải phương thức), mã gọi không có giá trị mặc định. Điều này có thể phù hợp nếu mã gọi thực sự không quan tâm đến tham số tùy chọn và ý nghĩa của nó. Trong trường hợp khác (tham số tùy chọn với giá trị mặc định), mã gọi đã biên dịch trước đó sẽ không bị ảnh hưởng khi giá trị mặc định thay đổi. Điều này cũng có thể thích hợp khi mã gọi thực sự quan tâm đến tham số; việc bỏ qua nó trong nguồn giống như nói, "giá trị mặc định được đề xuất hiện tại là OK đối với tôi."
stakx - không còn đóng góp vào

Điểm tôi đang cố gắng đưa ra ở đây là mặc dù có những hậu quả đối với một trong hai cách tiếp cận (như bạn đã chỉ ra), chúng vốn dĩ không phải là thuận lợi hay bất lợi. Điều đó cũng phụ thuộc vào nhu cầu và mục tiêu của mã gọi. Từ POV đó, tôi thấy phán quyết trong câu trả lời cuối cùng của bạn hơi quá cứng nhắc.
stakx - không còn đóng góp vào

@stakx: Tôi đã nói "tránh sử dụng" chứ không phải "không bao giờ sử dụng". Nếu thay đổi X sẽ có nghĩa là lần biên dịch lại tiếp theo của Y sẽ thay đổi hành vi của Y, điều đó sẽ yêu cầu cấu hình hệ thống xây dựng để mọi biên dịch lại của X cũng biên dịch lại Y (làm chậm mọi thứ) hoặc tạo ra rủi ro rằng một lập trình viên sẽ thay đổi X theo cách sẽ phá vỡ Y trong lần biên dịch tiếp theo và chỉ phát hiện ra sự phá vỡ như vậy sau đó khi Y bị thay đổi vì một số lý do hoàn toàn không liên quan. Các tham số mặc định chỉ nên được sử dụng khi lợi thế của chúng lớn hơn chi phí đó.
supercat

4

Có thể tranh luận xem có nên sử dụng các đối số tùy chọn hoặc quá tải hay không, nhưng quan trọng nhất, mỗi đối số đều có khu vực riêng mà chúng không thể thay thế được.

Các đối số tùy chọn, khi được sử dụng kết hợp với các đối số được đặt tên, sẽ cực kỳ hữu ích khi được kết hợp với một số đối số dài-danh sách-với-tất cả-tùy chọn của cuộc gọi COM.

Quá tải cực kỳ hữu ích khi phương thức có thể hoạt động trên nhiều loại đối số khác nhau (chỉ là một trong số các ví dụ) và thực hiện đúc nội bộ, chẳng hạn; bạn chỉ cần cung cấp cho nó bất kỳ kiểu dữ liệu nào phù hợp (được chấp nhận bởi một số quá tải hiện có). Không thể đánh bại điều đó với các đối số tùy chọn.


3

Tôi đang mong đợi các tham số tùy chọn vì nó giữ những gì mặc định gần với phương thức hơn. Vì vậy, thay vì hàng chục dòng cho quá tải chỉ gọi phương thức "mở rộng", bạn chỉ cần xác định phương thức một lần và bạn có thể xem các tham số tùy chọn được mặc định trong chữ ký phương thức. Tôi muốn nhìn vào:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

Thay vì điều này:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

Rõ ràng ví dụ này thực sự đơn giản nhưng trong trường hợp OP với 5 quá tải, mọi thứ có thể trở nên đông đúc thực sự nhanh chóng.


7
Tôi đã nghe nói các tham số tùy chọn nên ở cuối cùng, phải không?
Ilya Ryzhenkov 30/10/08

Phụ thuộc vào thiết kế của bạn. Có lẽ đối số 'bắt đầu' thường quan trọng, trừ khi nó không phải. Có lẽ bạn có cùng một chữ ký ở một nơi khác, có nghĩa là một cái gì đó khác nhau. Đối với một ví dụ tiếp theo, public Rectangle (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Robert P

13
Từ những gì họ đã nói trong bài nói chuyện, các thông số tùy chọn phải sau các thông số bắt buộc.
Greg Beech

3

Một trong những khía cạnh yêu thích của tôi về các tham số tùy chọn là bạn thấy điều gì sẽ xảy ra với các tham số của mình nếu bạn không cung cấp chúng, ngay cả khi không đi đến định nghĩa phương thức. Visual Studio sẽ chỉ cho bạn thấy giá trị mặc định cho tham số khi bạn nhập tên phương thức. Với một phương thức quá tải, bạn gặp khó khăn với việc đọc tài liệu (nếu thậm chí có sẵn) hoặc điều hướng trực tiếp đến định nghĩa của phương thức (nếu có) và phương thức mà quá tải kết thúc.

Cụ thể: nỗ lực tài liệu có thể tăng nhanh với số lượng quá tải và bạn có thể sẽ phải sao chép các nhận xét đã có từ các quá tải hiện có. Điều này khá khó chịu, vì nó không tạo ra bất kỳ giá trị nào và phá vỡ nguyên tắc KHÔ ). Mặt khác, với một tham số tùy chọn, có chính xác một nơi mà tất cả các tham số được ghi lại và bạn thấy ý nghĩa cũng như giá trị mặc định của chúng trong khi nhập.

Cuối cùng nhưng không kém phần quan trọng, nếu bạn là người sử dụng API, bạn thậm chí có thể không có tùy chọn kiểm tra chi tiết triển khai (nếu bạn không có mã nguồn) và do đó không có cơ hội để xem phương thức siêu nào mà phương thức bị quá tải đang gói. Vì vậy, bạn đang gặp khó khăn với việc đọc tài liệu và hy vọng rằng tất cả các giá trị mặc định được liệt kê ở đó, nhưng điều này không phải lúc nào cũng đúng.

Tất nhiên, đây không phải là một câu trả lời xử lý tất cả các khía cạnh, nhưng tôi nghĩ nó bổ sung một câu trả lời mà cho đến nay vẫn chưa được đề cập.


1

Mặc dù chúng (được cho là?) Là hai cách tương đương về mặt khái niệm có sẵn để bạn lập mô hình API của mình từ đầu, nhưng chúng không may có một số khác biệt nhỏ khi bạn cần xem xét khả năng tương thích ngược thời gian chạy cho các ứng dụng cũ của mình. Đồng nghiệp của tôi (cảm ơn Brent!) Đã chỉ cho tôi bài đăng tuyệt vời này : Các vấn đề về phiên bản với các đối số tùy chọn . Một số trích dẫn từ nó:

Lý do mà các tham số tùy chọn được đưa vào C # 4 ngay từ đầu là để hỗ trợ tương tác COM. Đó là nó. Và bây giờ, chúng tôi đang tìm hiểu về toàn bộ ý nghĩa của thực tế này. Nếu bạn có một phương thức với các tham số tùy chọn, bạn không bao giờ có thể thêm quá tải với các tham số tùy chọn bổ sung vì sợ gây ra thay đổi phá vỡ thời gian biên dịch. Và bạn không bao giờ có thể xóa quá tải hiện có, vì đây luôn là một thay đổi phá vỡ thời gian chạy. Bạn cần phải coi nó như một giao diện. Cách duy nhất của bạn trong trường hợp này là viết một phương thức mới với một tên mới. Vì vậy, hãy lưu ý điều này nếu bạn định sử dụng các đối số tùy chọn trong các API của mình.


1

Một cảnh báo trước của các tham số tùy chọn là lập phiên bản, trong đó một trình tái cấu trúc có những hậu quả không mong muốn. Một ví dụ:

Mã ban đầu

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

Giả sử đây là một trong nhiều trình gọi của phương pháp trên:

HandleError("Disk is full", false);

Ở đây sự kiện không im lặng và được coi là quan trọng.

Bây giờ, giả sử sau khi tái cấu trúc, chúng tôi nhận thấy rằng tất cả các lỗi đều nhắc người dùng, vì vậy chúng tôi không cần cờ im lặng nữa. Vì vậy, chúng tôi loại bỏ nó.

Sau khi tái cấu trúc

Cuộc gọi trước đây vẫn được biên dịch và giả sử nó trượt qua bộ tái cấu trúc không thay đổi:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

Bây giờ falsesẽ có ảnh hưởng không mong muốn, sự kiện sẽ không còn được coi là nghiêm trọng nữa.

Điều này có thể dẫn đến một lỗi nhỏ, vì sẽ không có lỗi biên dịch hoặc thời gian chạy (không giống như một số cảnh báo khác về tùy chọn, chẳng hạn như điều này hoặc điều này ).

Lưu ý rằng có nhiều dạng của cùng một vấn đề này. Một hình thức khác được nêu ở đây .

Cũng lưu ý rằng Nghiêm sử dụng tên thông số khi gọi phương pháp này sẽ tránh được vấn đề này, chẳng hạn như như thế này: HandleError("Disk is full", silent:false). Tuy nhiên, có thể không thực tế nếu cho rằng tất cả các nhà phát triển khác (hoặc người dùng API công khai) sẽ làm như vậy.

Vì những lý do này, tôi sẽ tránh sử dụng các tham số tùy chọn trong một API công khai (hoặc thậm chí là một phương thức công khai nếu nó có thể được sử dụng rộng rãi) trừ khi có những cân nhắc thuyết phục khác.


0

Cả tham số Tùy chọn, Quá tải phương thức đều có ưu điểm hoặc nhược điểm riêng. Tùy thuộc vào sở thích của bạn để lựa chọn giữa chúng.

Tham số tùy chọn: chỉ khả dụng trong .Net 4.0. tham số tùy chọn giảm kích thước mã của bạn. Bạn không thể xác định và tham số ref

Phương thức quá tải: Bạn có thể Xác định các tham số Out và ref. Kích thước mã sẽ tăng lên nhưng phương thức bị quá tải rất dễ hiểu.


0

Trong nhiều trường hợp, các tham số tùy chọn được sử dụng để chuyển đổi thực thi. Ví dụ:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

Tham số chiết khấu ở đây được sử dụng để cung cấp nguồn cấp dữ liệu cho câu lệnh if-then-else. Có một sự đa hình không được công nhận và sau đó nó được triển khai dưới dạng một câu lệnh if-then-else. Trong những trường hợp như vậy, tốt hơn là nên chia hai luồng kiểm soát thành hai phương pháp độc lập:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

Bằng cách này, chúng tôi thậm chí đã bảo vệ lớp không nhận được cuộc gọi với chiết khấu bằng không. Cuộc gọi đó có nghĩa là người gọi nghĩ rằng có chiết khấu, nhưng thực tế không có chiết khấu nào cả. Sự hiểu lầm như vậy có thể dễ dàng gây ra lỗi.

Trong những trường hợp như thế này, tôi không muốn có các tham số tùy chọn, nhưng buộc người gọi chọn rõ ràng kịch bản thực thi phù hợp với tình huống hiện tại của nó.

Tình huống rất giống với việc có các tham số có thể là null. Đó cũng là một ý tưởng tồi tệ không kém khi việc triển khai kết thúc với các câu lệnh như if (x == null).

Bạn có thể tìm thấy phân tích chi tiết trên các liên kết này: Tránh tham số tùy chọntránh tham số rỗng


0

Để thêm không cần trí tuệ khi nào sử dụng quá tải thay vì tùy chọn:

Bất cứ khi nào bạn có một số tham số chỉ có ý nghĩa với nhau, đừng giới thiệu các tùy chọn trên chúng.

Hay nói chung hơn, bất cứ khi nào chữ ký phương thức của bạn kích hoạt các mẫu sử dụng không hợp lý, hãy hạn chế số lần hoán vị của các lệnh gọi có thể. Ví dụ: bằng cách sử dụng quá tải thay vì tùy chọn (quy tắc này cũng đúng khi bạn có một số tham số của cùng một kiểu dữ liệu, nhân tiện, ở đây, các thiết bị như phương pháp gốc hoặc kiểu dữ liệu tùy chỉnh có thể giúp ích).

Thí dụ:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
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.