Cái nào tốt hơn, trả về giá trị hoặc tham số out?


147

Nếu chúng ta muốn nhận một giá trị từ một phương thức, chúng ta có thể sử dụng giá trị trả về, như sau:

public int GetValue(); 

hoặc là:

public void GetValue(out int x);

Tôi không thực sự hiểu sự khác biệt giữa chúng và vì vậy, không biết cái nào tốt hơn. Bạn có thể giải thích cho tôi điều này?

Cảm ơn bạn.


3
Tôi ước gì C # có nhiều giá trị trả về như Python chẳng hạn.
Bẫy

12
@Trap Bạn có thể trả về Tuplenếu bạn muốn, nhưng sự đồng thuận chung là nếu bạn cần trả lại nhiều hơn một thứ, mọi thứ thường liên quan đến nhau và mối quan hệ đó thường được thể hiện tốt nhất dưới dạng một lớp.
Pharap

2
@Pharap Tuples trong C # ở dạng hiện tại chỉ là xấu xí, nhưng đó chỉ là ý kiến ​​của tôi. Mặt khác, "sự đồng thuận chung" có nghĩa là không có gì từ quan điểm khả năng sử dụng và năng suất. Bạn sẽ không tạo một lớp để trả về một vài giá trị với cùng lý do bạn sẽ không tạo một lớp để trả về một vài giá trị dưới dạng tham số ref / out.
Bẫy

@Trap return Tuple.Create(x, y, z);Không phải là xấu xí. Bên cạnh đó, đến cuối ngày để giới thiệu chúng ở cấp độ ngôn ngữ. Lý do tôi sẽ không tạo một lớp để trả về các giá trị từ tham số ref / out là bởi vì các tham số ref / out chỉ thực sự có ý nghĩa đối với các cấu trúc có thể thay đổi lớn (như ma trận) hoặc các giá trị tùy chọn và điều này gây tranh cãi.
Pharap

Nhóm @Pharap C # đang tích cực tìm cách giới thiệu bộ dữ liệu ở cấp độ ngôn ngữ. Mặc dù rất hoan nghênh toàn bộ các tùy chọn trong .NET hiện đang áp đảo - loại ẩn danh, .NET Tuple<>và C # tuples!. Tôi chỉ muốn C # cho phép trả về các loại ẩn danh từ các phương thức với loại suy ra trình biên dịch (như autotrong Dlang).
nawfal

Câu trả lời:


153

Các giá trị trả về hầu như luôn là lựa chọn đúng khi phương thức không có gì khác để trả về. (Trong thực tế, tôi không thể nghĩ ra bất kỳ trường hợp tôi muốn bao giờ muốn có một phương pháp khoảng trống với một outtham số, nếu tôi có sự lựa chọn. C # 7 Deconstructphương pháp để giải cấu trúc ngôn ngữ được hỗ trợ đóng vai trò như một ngoại lệ rất, rất hiếm đối với quy tắc này .)

Ngoài bất cứ điều gì khác, nó ngăn người gọi phải khai báo biến riêng:

int foo;
GetValue(out foo);

đấu với

int foo = GetValue();

Các giá trị out cũng ngăn chặn chuỗi phương thức như thế này:

Console.WriteLine(GetValue().ToString("g"));

(Thật vậy, đó cũng là một trong những vấn đề với setters tài sản, và đó là lý do tại sao mẫu trình xây dựng sử dụng các phương thức trả về trình tạo, ví dụ myStringBuilder.Append(xxx).Append(yyy).)

Ngoài ra, các tham số out hơi khó sử dụng với sự phản chiếu và thường làm cho việc kiểm tra cũng khó hơn. (Nhiều nỗ lực hơn thường được đưa vào để dễ dàng giả định các giá trị trả về hơn các tham số ngoài). Về cơ bản không có gì tôi có thể nghĩ rằng họ làm cho dễ dàng hơn ...

Trả về giá trị FTW.

EDIT: Xét về những gì đang diễn ra ...

Về cơ bản khi bạn truyền vào một đối số cho tham số "out", bạn phải truyền vào một biến. (Các phần tử mảng cũng được phân loại là biến.) Phương thức bạn gọi không có biến "mới" trên ngăn xếp của nó cho tham số - nó sử dụng biến của bạn để lưu trữ. Bất kỳ thay đổi trong biến được nhìn thấy ngay lập tức. Đây là một ví dụ cho thấy sự khác biệt:

using System;

class Test
{
    static int value;

    static void ShowValue(string description)
    {
        Console.WriteLine(description + value);
    }

    static void Main()
    {
        Console.WriteLine("Return value test...");
        value = 5;
        value = ReturnValue();
        ShowValue("Value after ReturnValue(): ");

        value = 5;
        Console.WriteLine("Out parameter test...");
        OutParameter(out value);
        ShowValue("Value after OutParameter(): ");
    }

    static int ReturnValue()
    {
        ShowValue("ReturnValue (pre): ");
        int tmp = 10;
        ShowValue("ReturnValue (post): ");
        return tmp;
    }

    static void OutParameter(out int tmp)
    {
        ShowValue("OutParameter (pre): ");
        tmp = 10;
        ShowValue("OutParameter (post): ");
    }
}

Các kết quả:

Return value test...
ReturnValue (pre): 5
ReturnValue (post): 5
Value after ReturnValue(): 10
Out parameter test...
OutParameter (pre): 5
OutParameter (post): 10
Value after OutParameter(): 10

Sự khác biệt là ở bước "bài" - tức là sau khi biến cục bộ hoặc tham số đã được thay đổi. Trong thử nghiệm ReturnValue, điều này không tạo ra sự khác biệt với valuebiến tĩnh . Trong thử nghiệm OutParameter, valuebiến được thay đổi bởi dòngtmp = 10;


2
Bạn làm tôi tin rằng giá trị trả lại tốt hơn nhiều :). Nhưng tôi vẫn tự hỏi điều gì xảy ra "sâu". Ý tôi là, giá trị trả về và tham số out, chúng có khác nhau về cách chúng được tạo, gán và trả lại không?
Quan Mai

1
TryPude là ví dụ tốt nhất khi sử dụng param là phù hợp & sạch sẽ. Mặc dù tôi đã sử dụng nó trong các trường hợp đặc biệt như if (WorkSucceeded (out List <chuỗi> lỗi) về cơ bản giống với mẫu của TryPude
Chad Grant

2
Một câu trả lời không ích lợi. Như đã giải thích trong các ý kiến ​​về câu hỏi này , các tham số out có một số lợi thế hữu ích. Ưu tiên giữa out vs return nên phụ thuộc vào tình huống.
aaronsnoswell

2
@aaronsnoswell: Nhận xét chính xác nào? Ghi nhớ rằng các ví dụ về Dictionary.TryGetValuekhông áp dụng ở đây, vì đó không phải là một phương pháp vô hiệu. Bạn có thể giải thích lý do tại sao bạn muốn một outtham số thay vì giá trị trả về không? (Ngay cả đối với TryGetValue, tôi muốn một giá trị trả về, trong đó có tất cả các thông tin đầu ra, cá nhân Xem NodaTime của. ParseResult<T>Cho một ví dụ về cách tôi đã thiết kế nó.)
Jon Skeet

2
@Jim: Tôi đoán chúng ta sẽ phải đồng ý không đồng ý. Trong khi tôi thấy quan điểm của bạn về búa người trên đầu với điều này, nó cũng làm cho nó ít thân thiện đối với những người làm biết những gì họ đang làm. Điều hay ho về một ngoại lệ thay vì giá trị trả về là bạn không thể dễ dàng bỏ qua nó và tiếp tục như không có gì xảy ra ... trong khi với cả giá trị trả về và outtham số, bạn không thể làm gì với giá trị.
Jon Skeet

26

Những gì tốt hơn, phụ thuộc vào tình hình cụ thể của bạn. Một trong những lý do outtồn tại là để tạo điều kiện trả về nhiều giá trị từ một lệnh gọi phương thức:

public int ReturnMultiple(int input, out int output1, out int output2)
{
    output1 = input + 1;
    output2 = input + 2;

    return input;
}

Vì vậy, một định nghĩa không tốt hơn so với định nghĩa khác. Nhưng thông thường, bạn muốn sử dụng một khoản hoàn trả đơn giản, trừ khi bạn có tình huống trên chẳng hạn.

EDIT: Đây là một mẫu chứng minh một trong những lý do mà từ khóa tồn tại. Trên đây là không có cách nào để được coi là một thực hành tốt nhất.


2
Tôi không đồng ý với điều này, tại sao bạn không trả về cấu trúc dữ liệu với 4 số nguyên? Điều này thực sự khó hiểu.
Chad Grant

1
Rõ ràng có nhiều cách (và tốt hơn) để trả về nhiều giá trị, tôi chỉ cho OP một lý do tại sao tồn tại ở nơi đầu tiên.
pyrocumulus

1
Tôi đồng ý với @Cloud. Chỉ vì đó không phải là cách tốt nhất không có nghĩa là nó không tồn tại.
Cerebrus

Chà, mã thậm chí sẽ không biên dịch cho một ... và ít nhất nên đề cập rằng nó được coi là thực hành xấu / không được ưa thích.
Chad Grant

1
Nó sẽ không được biên dịch? Và tại sao vậy? Mẫu này có thể biên dịch hoàn hảo. Và xin vui lòng, tôi đang đưa ra một ví dụ về lý do tại sao một cú pháp / từ khóa cụ thể tồn tại, không phải là một bài học về thực tiễn tốt nhất.
pyrocumulus

23

Bạn thường nên thích một giá trị trả về hơn một param. Thông số ra là một điều ác cần thiết nếu bạn thấy mình viết mã cần làm 2 việc. Một ví dụ điển hình cho điều này là mẫu Thử (như Int32.TryPude).

Hãy xem xét người gọi hai phương thức của bạn sẽ phải làm gì. Ví dụ đầu tiên tôi có thể viết điều này ...

int foo = GetValue();

Lưu ý rằng tôi có thể khai báo một biến và gán nó thông qua phương thức của bạn trong một dòng. Ví dụ thứ 2 nó trông như thế này ...

int foo;
GetValue(out foo);

Bây giờ tôi buộc phải khai báo biến của mình lên phía trước và viết mã của mình qua hai dòng.

cập nhật

Một nơi tốt để xem khi hỏi những loại câu hỏi này là Nguyên tắc Thiết kế .NET Framework. Nếu bạn có phiên bản sách thì bạn có thể thấy các chú thích của Anders Hejlsberg và những người khác về chủ đề này (trang 184-185) nhưng phiên bản trực tuyến ở đây ...

http://msdn.microsoft.com/en-us/l Library / ms182131 (VS.80) .aspx

Nếu bạn thấy mình cần trả lại hai thứ từ một API thì việc gói chúng trong một cấu trúc / lớp sẽ tốt hơn một tham số ngoài.


Câu trả lời tuyệt vời, đặc biệt là tham chiếu đến TryPude, một hàm (phổ biến) buộc các nhà phát triển sử dụng các biến (không phổ biến).
Cerebrus

12

Có một lý do để sử dụng một thông outsố chưa được đề cập: phương thức gọi có nghĩa vụ phải nhận nó. Nếu phương thức của bạn tạo ra một giá trị mà người gọi không nên loại bỏ, làm cho nó trở thành một outngười gọi phải chấp nhận cụ thể:

 Method1();  // Return values can be discard quite easily, even accidentally

 int  resultCode;
 Method2(out resultCode);  // Out params are a little harder to ignore

Tất nhiên người gọi vẫn có thể bỏ qua giá trị trong một outparam, nhưng bạn đã gọi sự chú ý của họ đến nó.

Đây là một nhu cầu hiếm có; thường xuyên hơn, bạn nên sử dụng một ngoại lệ cho một vấn đề thực sự hoặc trả lại một đối tượng có thông tin trạng thái cho "FYI", nhưng có thể có trường hợp điều này rất quan trọng.


8

Đó là sở thích chủ yếu

Tôi thích trả về và nếu bạn có nhiều lợi nhuận, bạn có thể gói chúng trong Kết quả DTO

public class Result{
  public Person Person {get;set;}
  public int Sum {get;set;}
}

5

Bạn chỉ có thể có một giá trị trả về trong khi bạn có thể có nhiều tham số.

Bạn chỉ cần xem xét các tham số trong những trường hợp đó.

Tuy nhiên, nếu bạn cần trả về nhiều hơn một tham số từ phương thức của mình, bạn có thể muốn xem những gì bạn đang trả về từ cách tiếp cận OO và xem xét liệu bạn có nên trả lại một đối tượng hoặc một cấu trúc với các tham số này không. Do đó, bạn trở lại giá trị trả lại một lần nữa.


5

Bạn hầu như luôn luôn nên sử dụng giá trị trả về. Các outtham số '' tạo ra một chút ma sát với rất nhiều API, thành phần, v.v.

Ngoại lệ đáng chú ý nhất xuất hiện trong đầu là khi bạn muốn trả về nhiều giá trị (.Net Framework không có bộ dữ liệu cho đến 4.0), chẳng hạn như với TryParsemẫu.


Không chắc chắn nếu đó là một thực tiễn tốt nhưng Arraylist cũng có thể được sử dụng để trả về nhiều giá trị.
BA

@BA không phải là một cách thực hành tốt, những người dùng khác sẽ không biết Arraylist này chứa gì, hoặc họ đang ở vị trí nào. Ví dụ tôi muốn trả về số lượng byte đã đọc và cả giá trị. ra hoặc tuples được đặt tên sẽ tốt hơn nhiều. ArrayList, hoặc bất kỳ danh sách nào khác sẽ chỉ hữu ích nếu bạn muốn trả về một sự va chạm của mọi thứ, ví dụ như một danh sách những người.
sLw

2

Tôi thích cái sau thay vì một trong những cái trong ví dụ đơn giản này.

public int Value
{
    get;
    private set;
}

Nhưng, tất cả chúng đều rất giống nhau. Thông thường, người ta sẽ chỉ sử dụng 'out' nếu họ cần truyền lại nhiều giá trị từ phương thức. Nếu bạn muốn gửi một giá trị vào và ra khỏi phương thức, người ta sẽ chọn 'ref'. Phương pháp của tôi là tốt nhất, nếu bạn chỉ trả về một giá trị, nhưng nếu bạn muốn truyền tham số và lấy lại giá trị, bạn có thể chọn lựa chọn đầu tiên.


2

Tôi nghĩ rằng một trong số ít trường hợp sẽ hữu ích khi làm việc với bộ nhớ không được quản lý và bạn muốn làm rõ rằng giá trị "được trả về" nên được xử lý theo cách thủ công, thay vì tự xử lý nó .


2

Ngoài ra, các giá trị trả về tương thích với các mô hình thiết kế không đồng bộ.

Bạn không thể chỉ định một chức năng "không đồng bộ" nếu nó sử dụng tham số ref hoặc out.

Tóm lại , Giá trị trả về cho phép kết nối phương thức, cú pháp sạch hơn (bằng cách loại bỏ sự cần thiết cho người gọi để khai báo các biến bổ sung) và cho phép thiết kế không đồng bộ mà không cần sửa đổi đáng kể trong tương lai.


Điểm tuyệt vời trong chỉ định "không đồng bộ". Nghĩ rằng người khác sẽ đề cập đến điều đó. Xâu chuỗi - cũng như sử dụng giá trị trả về (thực sự là chính hàm) làm biểu thức là một lợi ích chính khác. Đó thực sự là sự khác biệt giữa việc chỉ xem xét cách lấy lại nội dung từ một quy trình ("nhóm" - thảo luận Tuple / class / struct) và xử lý chính quy trình đó như một biểu thức có thể được thay thế cho một giá trị duy nhất (tính hữu ích của chức năng chính nó, bởi vì nó chỉ trả về 1 giá trị).
dùng1172173

1

Cả hai đều có một mục đích khác nhau và không được trình biên dịch đối xử giống nhau. Nếu phương thức của bạn cần trả về một giá trị, thì bạn phải sử dụng return. Out được sử dụng khi phương thức của bạn cần trả về nhiều giá trị.

Nếu bạn sử dụng return, thì dữ liệu trước tiên được ghi vào ngăn xếp phương thức và sau đó trong phương thức gọi. Trong trường hợp hết, nó được ghi trực tiếp vào ngăn xếp phương thức gọi. Không chắc chắn nếu có thêm sự khác biệt.


Các phương pháp ngăn xếp? Tôi không phải là chuyên gia c #, nhưng x86 chỉ hỗ trợ một ngăn xếp cho mỗi luồng. "Khung" của phương thức được sắp xếp lại trong khi trả về và nếu chuyển đổi ngữ cảnh xảy ra thì ngăn xếp được giải phóng có thể được ghi đè. Trong c tất cả các giá trị trả về đi trong registry eax. Nếu bạn muốn trả về các đối tượng / cấu trúc, chúng cần được phân bổ cho heap và con trỏ sẽ được đặt trong eax.
Stefan Lundström

1

Như những người khác đã nói: giá trị trả lại, không ra param.

Tôi có thể giới thiệu cho bạn cuốn sách "Nguyên tắc thiết kế khung" (tái bản lần 2) không? Trang 184-185 bao gồm các lý do để tránh các thông số. Toàn bộ cuốn sách sẽ giúp bạn đi đúng hướng trong tất cả các loại vấn đề mã hóa .NET.

Đồng minh với Nguyên tắc thiết kế khung là việc sử dụng công cụ phân tích tĩnh, FxCop. Bạn sẽ tìm thấy điều này trên các trang web của Microsoft dưới dạng tải xuống miễn phí. Chạy mã này trên mã được biên dịch của bạn và xem những gì nó nói. Nếu nó phàn nàn về hàng trăm và hàng trăm thứ ... đừng hoảng sợ! Hãy bình tĩnh và cẩn thận với những gì nó nói về từng trường hợp. Đừng vội vàng sửa chữa mọi thứ càng sớm càng tốt. Học hỏi từ những gì nó đang nói với bạn. Bạn sẽ được đưa vào con đường để làm chủ.


1

Sử dụng từ khóa out với loại bool trả về, đôi khi có thể làm giảm sự phình mã và tăng khả năng đọc. (Chủ yếu khi thông tin bổ sung trong thông số ngoài thường bị bỏ qua.) Ví dụ:

var result = DoThing();
if (result.Success)
{
    result = DoOtherThing()
    if (result.Success)
    {
        result = DoFinalThing()
        if (result.Success)
        {
            success = true;
        }
    }
}

vs

var result;
if (DoThing(out result))
{
    if (DoOtherThing(out result))
    {
        if (DoFinalThing(out result))
        {
            success = true;
        }
    }
}

1

Không có sự khác biệt thực sự. Các tham số out nằm trong C # để cho phép phương thức trả về nhiều hơn một giá trị, đó là tất cả.

Tuy nhiên, có một số khác biệt nhỏ, nhưng không phải trong số chúng thực sự quan trọng:

Sử dụng tham số out sẽ bắt buộc bạn sử dụng hai dòng như:

int n;
GetValue(n);

trong khi sử dụng giá trị trả về sẽ cho phép bạn thực hiện trong một dòng:

int n = GetValue();

Một điểm khác biệt (chỉ đúng với các loại giá trị và chỉ khi C # không thực hiện hàm) là việc sử dụng giá trị trả về sẽ nhất thiết phải tạo một bản sao của giá trị khi hàm trả về, trong khi sử dụng tham số OUT sẽ không nhất thiết phải làm như vậy.


0

out hữu ích hơn khi bạn đang cố trả về một đối tượng mà bạn khai báo trong phương thức.

Thí dụ

public BookList Find(string key)
{
   BookList book; //BookList is a model class
   _books.TryGetValue(key, out book) //_books is a concurrent dictionary
                                     //TryGetValue gets an item with matching key and returns it into book.
   return book;
}

0

giá trị trả vềgiá trị bình thường được trả về bởi phương thức của bạn.

Trong trường hợp tham số out out , well out và ref là 2 từ khóa của C #, chúng cho phép truyền các biến làm tham chiếu .

Sự khác biệt lớn giữa refout là, ref nên được khởi tạo trước và sau không


-2

Tôi nghi ngờ tôi sẽ không xem xét câu hỏi này, nhưng tôi là một lập trình viên rất có kinh nghiệm và tôi hy vọng một số độc giả cởi mở hơn sẽ chú ý.

Tôi tin rằng nó phù hợp với các ngôn ngữ lập trình hướng đối tượng tốt hơn cho các thủ tục trả về giá trị (VRP) của chúng là mang tính xác định và thuần túy.

'VRP' là tên học thuật hiện đại cho một hàm được gọi là một phần của biểu thức và có giá trị trả về thay thế cho cuộc gọi trong khi đánh giá biểu thức. Ví dụ, trong một tuyên bố như x = 1 + f(y)hàm fđang phục vụ như một VRP.

'Xác định' có nghĩa là kết quả của hàm chỉ phụ thuộc vào các giá trị của tham số. Nếu bạn gọi nó một lần nữa với cùng các giá trị tham số, bạn chắc chắn sẽ nhận được kết quả tương tự.

'Pure' có nghĩa là không có tác dụng phụ: gọi hàm không làm gì ngoài việc tính toán kết quả. Điều này có thể được hiểu là không có tác dụng phụ quan trọng , trong thực tế, do đó, nếu VRP xuất ra một thông báo gỡ lỗi mỗi khi nó được gọi, ví dụ, điều đó có thể bị bỏ qua.

Do đó, nếu trong C #, hàm của bạn không xác định và thuần túy, tôi nói bạn nên biến nó thành voidhàm (nói cách khác, không phải VRP) và bất kỳ giá trị nào nó cần trả về phải được trả về trong một outhoặc một reftham số.

Ví dụ: nếu bạn có chức năng xóa một số hàng khỏi bảng cơ sở dữ liệu và bạn muốn nó trả về số lượng hàng đã xóa, bạn nên khai báo nó như sau:

public void DeleteBasketItems(BasketItemCategory category, out int count);

Nếu đôi khi bạn muốn gọi hàm này nhưng không nhận được count, bạn luôn có thể khai báo quá tải.

Bạn có thể muốn biết tại sao phong cách này phù hợp với lập trình hướng đối tượng tốt hơn. Nhìn rộng ra, nó phù hợp với một phong cách lập trình có thể (một chút không chính xác) được gọi là "lập trình thủ tục", và đó là một phong cách lập trình thủ tục phù hợp với lập trình hướng đối tượng tốt hơn.

Tại sao? Mô hình cổ điển của các đối tượng là chúng có các thuộc tính (còn gọi là thuộc tính), và bạn thẩm vấn và thao tác đối tượng (chủ yếu) thông qua việc đọc và cập nhật các thuộc tính đó. Một kiểu lập trình thủ tục có xu hướng làm cho việc này dễ dàng hơn, bởi vì bạn có thể thực thi mã tùy ý ở giữa các hoạt động nhận và đặt thuộc tính.

Nhược điểm của lập trình thủ tục là bởi vì bạn có thể thực thi mã tùy ý ở mọi nơi, bạn có thể nhận được một số tương tác rất khó hiểu và dễ bị lỗi thông qua các biến toàn cầu và tác dụng phụ.

Vì vậy, khá đơn giản, đó là một thực tiễn tốt để báo hiệu cho ai đó đọc mã của bạn rằng một hàm có thể có tác dụng phụ bằng cách làm cho nó không trả về giá trị.


> Nếu đôi khi bạn muốn gọi hàm này nhưng không được đếm, bạn luôn có thể khai báo quá tải. Trong phiên bản C # 7 (tôi nghĩ) và sau đó, bạn có thể sử dụng _biểu tượng loại bỏ để bỏ qua một tham số out, ví dụ: DeleteBasketItems (danh mục, out _);
người tranh luậ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.