Đang chờ nhiều Nhiệm vụ với kết quả khác nhau


237

Tôi có 3 nhiệm vụ:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Tất cả đều cần chạy trước khi mã của tôi có thể tiếp tục và tôi cũng cần kết quả từ mỗi mã. Không có kết quả nào có điểm chung với nhau

Làm thế nào để tôi gọi và chờ 3 nhiệm vụ hoàn thành và sau đó nhận được kết quả?


25
Bạn có bất kỳ yêu cầu đặt hàng? Đó là, bạn có muốn không bán nhà cho đến khi mèo được cho ăn?
Eric Lippert

Câu trả lời:


411

Sau khi sử dụng WhenAll, bạn có thể kéo các kết quả ra riêng với await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Bạn cũng có thể sử dụng Task.Result(vì bạn biết rằng đến thời điểm này họ đã hoàn thành thành công). Tuy nhiên, tôi khuyên bạn nên sử dụng awaitvì nó rõ ràng chính xác, trong khi Resultcó thể gây ra sự cố trong các tình huống khác.


83
Bạn chỉ có thể loại bỏ WhenAllhoàn toàn khỏi điều này; sự chờ đợi sẽ đảm bảo đảm bảo bạn không vượt qua 3 nhiệm vụ sau cho đến khi hoàn thành nhiệm vụ.
Phục vụ

134
Task.WhenAll()cho phép chạy tác vụ ở chế độ song song . Tôi không thể hiểu tại sao @Servy đã đề nghị xóa nó. Nếu không có WhenAllhọ sẽ được điều hành từng người một
Serge G.

86
@Sergey: Các tác vụ bắt đầu thực thi ngay lập tức. Ví dụ, catTaskđã chạy theo thời gian nó trở về FeedCat. Vì vậy, một trong hai cách tiếp cận sẽ có hiệu quả - câu hỏi duy nhất là liệu bạn muốn awaitchúng cùng một lúc hay tất cả cùng nhau. Việc xử lý lỗi hơi khác một chút - nếu bạn sử dụng Task.WhenAll, thì awaittất cả sẽ được xử lý , ngay cả khi một trong số đó bị lỗi sớm.
Stephen Cleary

23
@Sergey Gọi WhenAllkhông có tác động khi hoạt động thực thi hoặc cách chúng thực thi. Nó chỉ có bất kỳ khả năng ảnh hưởng như thế nào đến kết quả được quan sát. Trong trường hợp cụ thể này, sự khác biệt duy nhất là một lỗi trong một trong hai phương thức đầu tiên sẽ dẫn đến ngoại lệ bị ném trong ngăn xếp cuộc gọi này sớm hơn phương thức của tôi so với Stephen (mặc dù lỗi tương tự sẽ luôn bị ném, nếu có bất kỳ lỗi nào ).
Phục vụ

36
@Sergey: Điều quan trọng là các phương thức không đồng bộ luôn trả về các tác vụ "nóng" (đã bắt đầu).
Stephen Cleary

99

Chỉ awaitba nhiệm vụ riêng biệt, sau khi bắt đầu tất cả.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

8
@Bargitta Không, đó là sai. Họ sẽ làm công việc của họ song song. Hãy chạy nó và xem cho chính mình.
Phục vụ

5
Mọi người cứ hỏi cùng một câu hỏi sau nhiều năm ... Tôi cảm thấy điều quan trọng là phải nhấn mạnh lại rằng một nhiệm vụ " bắt đầu được tạo ra " trong cơ thể của câu trả lời : có thể họ không bận tâm đọc bình luận

9
@StephenYork Thêm Task.WhenAllthay đổi theo nghĩa đen không có gì về hành vi của chương trình, theo bất kỳ cách nào có thể quan sát được. Đó là một cuộc gọi phương thức hoàn toàn dư thừa. Bạn có thể thêm nó, nếu bạn muốn, như một lựa chọn thẩm mỹ, nhưng nó không thay đổi những gì mã làm. Thời gian thực thi của mã sẽ giống hệt hoặc không có cuộc gọi phương thức đó (tốt, về mặt kỹ thuật sẽ có một chi phí thực sự nhỏ để gọi WhenAll, nhưng điều này không đáng kể), chỉ khiến phiên bản đó chạy lâu hơn phiên bản này một chút .
Phục vụ

4
@StephenYork Ví dụ của bạn chạy các hoạt động tuần tự vì hai lý do. Các phương thức không đồng bộ của bạn không thực sự không đồng bộ, chúng đồng bộ. Việc bạn có các phương thức đồng bộ luôn trả về các tác vụ đã hoàn thành sẽ ngăn chúng chạy đồng thời. Tiếp theo, bạn không thực sự làm những gì được hiển thị trong câu trả lời này khi bắt đầu cả ba phương thức không đồng bộ, và sau đó chờ đợi ba nhiệm vụ lần lượt. Ví dụ của bạn không gọi từng phương thức cho đến khi kết thúc trước đó, do đó rõ ràng ngăn không cho bắt đầu một phương thức cho đến khi kết thúc trước đó, không giống như mã này.
Phục vụ

4
@MarcvanNieuwenhuijzen Điều đó không đúng, như đã được thảo luận trong các ý kiến ​​ở đây, và về các câu trả lời khác. Thêm WhenAlllà một thay đổi hoàn toàn thẩm mỹ. Sự khác biệt duy nhất có thể quan sát được trong hành vi là liệu bạn có đợi các nhiệm vụ sau kết thúc hay không nếu một tác vụ trước đó bị lỗi, điều mà thường không cần phải làm. Nếu bạn không tin vào nhiều lời giải thích cho lý do tại sao tuyên bố của bạn không đúng, bạn có thể tự mình chạy mã và thấy rằng điều đó không đúng.
Phục vụ

37

Nếu bạn đang sử dụng C # 7, bạn có thể sử dụng phương pháp trình bao bọc tiện dụng như thế này ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... Để kích hoạt cú pháp thuận tiện như thế này khi bạn muốn chờ đợi trên nhiều tác vụ với các kiểu trả về khác nhau. Tất nhiên, bạn sẽ phải thực hiện nhiều lần quá tải cho số lượng nhiệm vụ khác nhau để chờ đợi.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Tuy nhiên, hãy xem câu trả lời của Marc Gravell để biết một số tối ưu hóa xung quanh ValueTask và các nhiệm vụ đã hoàn thành nếu bạn có ý định biến ví dụ này thành một cái gì đó thực sự.


Tuples là tính năng C # 7 duy nhất có liên quan ở đây. Đó là chắc chắn trong bản phát hành cuối cùng.
Joel Mueller

Tôi biết về bộ dữ liệu và c # 7. Ý tôi là tôi không thể tìm thấy phương thức Khi Tất cả trả về bộ dữ liệu. Không gian tên / gói nào?
Yury Scherbakov

@YuryShcherbakov Task.WhenAll()không trả lại một tuple. Một đang được xây dựng từ các Resultthuộc tính của các nhiệm vụ được cung cấp sau khi nhiệm vụ được Task.WhenAll()hoàn thành.
Chris Charabaruk

2
Tôi đề nghị thay thế các .Resultcuộc gọi theo lý luận của Stephen để tránh những người khác tiếp tục thực hành xấu bằng cách sao chép ví dụ của bạn.
julealgon

Tôi tự hỏi tại sao phương pháp này không phải là một phần của khung? Có vẻ như rất hữu ích. Có phải họ đã hết thời gian và phải dừng lại ở một loại trả lại duy nhất?
Ian Grainger

14

Với ba nhiệm vụ - FeedCat(), SellHouse()BuyCar(), có hai trường hợp thú vị: hoặc là họ tất cả đồng bộ đầy đủ (đối với một số lý do, có lẽ bộ nhớ đệm hoặc một lỗi), hoặc họ không.

Hãy nói rằng chúng ta có, từ câu hỏi:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Bây giờ, một cách tiếp cận đơn giản sẽ là:

Task.WhenAll(x, y, z);

nhưng ... điều đó không thuận tiện để xử lý kết quả; chúng tôi thường muốn awaitđiều đó:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

nhưng điều này không có nhiều chi phí và phân bổ các mảng khác nhau (bao gồm cả params Task[]mảng) và danh sách (nội bộ). Nó hoạt động, nhưng nó không phải là IMO tuyệt vời. Theo nhiều cách, việc sử dụng một thao tác sẽ đơn giản hơnasync và chỉ awaitlần lượt từng thao tác :

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Trái với một số ý kiến trên, sử dụng awaitthay vì Task.WhenAlllàm cho có sự khác biệt như thế nào các nhiệm vụ chạy (đồng thời, liên tục, vv). Ở mức cao nhất, Task.WhenAll có trước hỗ trợ trình biên dịch tốt cho async/ awaitvà rất hữu ích khi những thứ đó không tồn tại . Nó cũng hữu ích khi bạn có một loạt các nhiệm vụ tùy ý, thay vì 3 nhiệm vụ kín đáo.

Nhưng: chúng ta vẫn có vấn đề là async/ awaittạo ra nhiều tiếng ồn của trình biên dịch để tiếp tục. Nếu có khả năng các tác vụ có thể thực sự hoàn thành đồng bộ, thì chúng ta có thể tối ưu hóa điều này bằng cách xây dựng theo một đường dẫn đồng bộ với dự phòng không đồng bộ:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Phương pháp "đường dẫn đồng bộ hóa với dự phòng không đồng bộ" này ngày càng phổ biến, đặc biệt là trong mã hiệu suất cao, nơi việc hoàn thành đồng bộ là tương đối thường xuyên. Lưu ý rằng nó sẽ không giúp ích gì cả nếu việc hoàn thành luôn không đồng bộ.

Những điều bổ sung áp dụng ở đây:

  1. với C # gần đây, một mẫu chung cho asyncphương thức dự phòng thường được triển khai như một hàm cục bộ:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. thích ValueTask<T>để Task<T>nếu có một cơ hội tốt cho những thứ không bao giờ hoàn toàn đồng bộ với nhiều giá trị lợi nhuận khác nhau:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. nếu có thể, thích IsCompletedSuccessfullyđến Status == TaskStatus.RanToCompletion; điều này hiện tồn tại trong .NET Core cho Taskvà ở mọi nơi choValueTask<T>


"Trái ngược với các câu trả lời khác nhau ở đây, sử dụng chờ đợi thay vì Nhiệm vụ. Khi tất cả không có gì khác biệt so với cách các tác vụ chạy (đồng thời, tuần tự, v.v.)" Tôi không thấy bất kỳ câu trả lời nào nói điều đó. Tôi đã nhận xét về họ nói nhiều như họ đã làm. Có rất nhiều ý kiến ​​về rất nhiều câu trả lời nói rằng, nhưng không có câu trả lời. Bạn đang đề cập đến cái gì? Cũng lưu ý rằng câu trả lời của bạn không xử lý kết quả của các nhiệm vụ (hoặc đối phó với thực tế là các kết quả đều thuộc một loại khác). Bạn đã soạn chúng theo một phương thức chỉ trả về một Taskkhi tất cả đã hoàn thành mà không sử dụng kết quả.
Phục vụ

@Servy bạn nói đúng, đó là ý kiến; Tôi sẽ thêm một tinh chỉnh để hiển thị bằng kết quả
Marc Gravell

Đã chỉnh sửa @Servy
Marc Gravell

Ngoài ra, nếu bạn sắp hoàn thành sớm các nhiệm vụ đồng bộ, bạn cũng có thể xử lý bất kỳ tác vụ nào bị hủy hoặc bị lỗi một cách đồng bộ, thay vì chỉ hoàn thành thành công. Nếu bạn đã đưa ra quyết định rằng đó là một sự tối ưu hóa mà chương trình của bạn cần (sẽ rất hiếm, nhưng sẽ xảy ra) thì bạn cũng có thể đi bằng mọi cách.
Phục vụ

@Servy đó là một chủ đề phức tạp - bạn có được ngữ nghĩa ngoại lệ khác nhau từ hai kịch bản - đang chờ để kích hoạt một ngoại lệ hoạt động khác với truy cập .Result để kích hoạt ngoại lệ. IMO tại thời điểm đó chúng ta nênawait có được ngữ nghĩa ngoại lệ "tốt hơn", với giả định rằng các ngoại lệ là hiếm nhưng có ý nghĩa
Marc Gravell

12

Bạn có thể lưu trữ chúng trong các nhiệm vụ, sau đó chờ đợi tất cả:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;

không var catTask = FeedCat()thực thi chức năng FeedCat()và lưu trữ kết quả để catTasklàm cho await Task.WhenAll()loại phần vô dụng vì phương thức đã được thực thi ??
Kraang Prime

1
@sanuel nếu họ trả lại nhiệm vụ <t>, thì không ... họ bắt đầu mở async, nhưng đừng chờ đợi nó
Reed Copsey

Tôi không nghĩ rằng điều này là chính xác, vui lòng xem các cuộc thảo luận dưới câu trả lời của @ StephenCleary ... cũng xem câu trả lời của Servy.
Rosdi Kasim

1
nếu tôi cần thêm .ConfigrtueAwait (false). Tôi sẽ thêm nó vào Chỉ nhiệm vụ. Khi nào hoặc cho mỗi người phục vụ tiếp theo?
AstroSharp

@AstroSharp nói chung, nên thêm nó vào tất cả chúng (nếu hoàn thành lần đầu tiên, nó sẽ bị bỏ qua một cách hiệu quả), nhưng trong trường hợp này, có lẽ bạn chỉ nên làm điều đầu tiên - trừ khi có nhiều sự đồng bộ hơn chuyện xảy ra sau này
Sậy Copsey

6

Trong trường hợp bạn đang cố gắng ghi nhật ký tất cả các lỗi, hãy đảm bảo rằng bạn giữ dòng Task.When ALL trong mã của bạn, rất nhiều ý kiến ​​cho rằng bạn có thể xóa nó và chờ đợi các tác vụ riêng lẻ. Task.WhenAll thực sự quan trọng để xử lý lỗi. Không có dòng này, bạn có khả năng để mã của bạn mở cho các ngoại lệ không quan sát được.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Hãy tưởng tượng FeedCat ném ngoại lệ trong đoạn mã sau:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Trong trường hợp đó, bạn sẽ không bao giờ chờ đợi trên houseTask hay carTask. Có 3 tình huống có thể xảy ra ở đây:

  1. SellHouse đã hoàn thành thành công khi FeedCat thất bại. Trong trường hợp này bạn ổn.

  2. SellHouse không hoàn thành và thất bại với ngoại lệ tại một số điểm. Ngoại lệ không được quan sát và sẽ được truy xuất lại trên luồng hoàn thiện.

  3. SellHouse chưa hoàn thành và chứa đựng sự chờ đợi bên trong nó. Trong trường hợp mã của bạn chạy trong ASP.NET SellHouse sẽ thất bại ngay khi một số chờ đợi sẽ hoàn thành bên trong nó. Điều này xảy ra vì về cơ bản, bạn đã thực hiện cuộc gọi và quên cuộc gọi và bối cảnh đồng bộ hóa bị mất ngay sau khi FeedCat thất bại.

Đây là lỗi mà bạn sẽ nhận được cho trường hợp (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

Đối với trường hợp (2), bạn sẽ gặp lỗi tương tự nhưng với dấu vết ngăn xếp ngoại lệ ban đầu.

Đối với .NET 4.0 trở lên, bạn có thể bắt gặp các ngoại lệ không quan sát được bằng cách sử dụng TaskScheduler.UnobservedTaskException. Đối với .NET 4.5 và các ngoại lệ không quan sát được mặc định sẽ bị nuốt theo mặc định đối với ngoại lệ không quan sát được .NET 4.0 sẽ làm hỏng quá trình của bạn.

Thêm chi tiết tại đây: Xử lý ngoại lệ tác vụ trong .NET 4.5


2

Bạn có thể sử dụng Task.WhenAllnhư đã đề cập, hoặc Task.WaitAll, tùy thuộc vào việc bạn có muốn chờ đợi chuỗi không. Hãy nhìn vào liên kết để giải thích cho cả hai.

Chờ tất cả so với khi nào


2

Sử dụng Task.WhenAllvà sau đó chờ kết quả:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.

mm ... không phải là Task.Value (có lẽ nó đã từng tồn tại vào năm 2013?), thay vì tCat.Result, tHouse.Result hoặc tCar.Result
Stephen York

1

Cảnh báo chuyển tiếp

Chỉ cần một headup nhanh cho những người truy cập này và các chủ đề tương tự khác đang tìm cách song song hóa EntityFramework bằng cách sử dụng async + await + task tool-set : Tuy nhiên, mô hình được hiển thị ở đây là âm thanh của bông tuyết đặc biệt của EF, bạn sẽ không đạt được sự thực thi song song trừ khi và cho đến khi bạn sử dụng một cá thể ngữ cảnh db (mới) riêng biệt bên trong mỗi cuộc gọi * Async () liên quan.

Loại điều này là cần thiết do các giới hạn thiết kế vốn có của bối cảnh ef-db, cấm chạy nhiều truy vấn song song trong cùng một thể hiện bối cảnh ef-db.


Tận dụng các câu trả lời đã được đưa ra, đây là cách để đảm bảo rằng bạn thu thập tất cả các giá trị ngay cả trong trường hợp một hoặc nhiều nhiệm vụ dẫn đến ngoại lệ:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Một triển khai thay thế có ít nhiều đặc điểm hiệu suất giống nhau có thể là:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }

-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

nếu bạn muốn truy cập Cat, bạn làm điều này:

var ct = (Cat)dn[0];

Điều này rất đơn giản để làm và rất hữu ích để sử dụng, không cần phải đi sau một giải pháp phức tạp.


1
Chỉ có một vấn đề với điều này: dynamiclà ma quỷ. Nó không phù hợp với COM, và không nên sử dụng trong mọi trường hợp không thực sự cần thiết. Đặc biệt nếu bạn quan tâm đến hiệu suất. Hoặc loại an toàn. Hoặc tái cấu trúc. Hoặc gỡ lỗi.
Joel Mueller
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.