+2

Làm thế nào return về nhiều giá trị trong C#?

Có bao giờ bạn thắc mắc làm thế nào để return nhiều giá trị về cùng 1 lúc trong C# như Javascript hay không. Thật ra, C# đã có tính năng này, và thậm chí có kha khá cách tiếp cận để giải quyết vấn đề này nữa là. Bây giờ bạn cùng mình, chúng ta cùng đi vào bài viết để tìm hiểu nhé.

Ví dụ chúng ta có bài toán, làm thế nào tìm ra số lớn nhất và nhỏ nhất từ list số nguyên bằng cách chỉ viết 1 hàm duy nhất? Cùng nhìn qua các cách tiếp cận dưới đây nhé.

Dùng Tuple

Đầu tiên ở JS có kỹ thuật destructuring thì C# có Tuple để làm điều tương tự. Một hàm thường chỉ return 1 kiểu giá trị thôi đúng không như với Tuple nó cho phép return nhiều hơn 😃

SystemTuple

Vì phiên bản này dùng object và hơi dài dòng nên mình sẽ gọi nó là biến thể dài dòng Hải Phòng. Cùng đi vào ví dụ nhé.

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
  // return về Tuple object
  public static Tuple<int, int> FindMaxMin(List<int> numbers)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    return Tuple.Create(max, min);
    // return new Tuple<int, int>(max, min); // như vầy cũng được nhá
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var (max, min) = FindMaxMin(numbers);
    var t = FindMaxMin(numbers);

    Console.WriteLine($"Max = {max}, Min = {min}"); // Max = 100, Min = -1
    Console.WriteLine($"Max = {t.Item1}, Min = {t.Item2}"); // Max = 100, Min = -1
  }
}

ValueTuple

Vì nó dài dòng như vậy, nên các bác kỹ sư đã đẻ ra 1 cái cú pháp khác ngắn gọn hơn, biến thể đầu cắt moi. Ví dụ nhé.

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  // return về tuples
  public static (int max, int min) FindMaxMin(List<int> numbers)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    return (max, min);
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var (max, min) = FindMaxMin(numbers); // destructuring
    
    Console.WriteLine($"Max = {max}, Min = {min}"); // Max = 100, Min = -1
  }
}

Bonus cho phần này, mình muốn nói về cú pháp ngắn gọn hơn nữa, biến thể đầu trọc, nhưng bản chất là một với cái ở trên.

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  // không dùng tên đại diện nữa
  public static (int, int) FindMaxMin(List<int> numbers)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    return (max, min);
  }
  
  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var (max, min) = FindMaxMin(numbers); // destructuring
    var t1 = FindMaxMin(numbers);
    (int, int) t2 = FindMaxMin(numbers); // destructuring với kiểu tường minh
    (int max, int min) t3 = FindMaxMin(numbers); // destructuring với tên tường minh
    
    var t4 = (max, min); // gom lại (assignment)
    (int max, int min) t5 = (max, min); // gom lại với tên

    Console.WriteLine($"Max = {max}, Min = {min}"); // Max = 100, Min = -1
    Console.WriteLine($"Max = {t1.Item1}, Min = {t1.Item2}"); // Max = 100, Min = -1
    Console.WriteLine($"Max = {t2.Item1}, Min = {t2.Item2}"); // Max = 100, Min = -1
    
    Console.WriteLine($"Max = {t4.Item1}, Min = {t4.Item2}"); // Max = 100, Min = -1
    Console.WriteLine($"Max = {t5.max}, Min = {t5.min}"); // Max = 100, Min = -1
  }
}

Khác nhau

Phiên bản ValueTuple và SystemTuple thọat nhìn chúng ta dễ thấy rằng chúng giống nhau (kết quả nhận về), nên có thể bạn sẽ nghĩ chúng là 1 ha. Nhưng thực tình là chúng có điểm khác biệt, điểm khác biệt lớn nhất đó là ValueTuple là dạng tham trị, còn SystemTuple sẽ là dạng tham chiếu. Nên nếu đem compare, sẽ cho kết quả khác nhau. Ví dụ:

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  public static Tuple<int, int> FindMaxMin(List<int> numbers)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    return Tuple.Create(max, min);
  }
  
  public static (int max, int min) FindMaxMinValueTuple(List<int> numbers)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    return (max, min);
  }
  
  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var t = FindMaxMin(numbers);
    var t1 = FindMaxMin(numbers);
    var t2 = FindMaxMinValueTuple(numbers);
    var t3 = FindMaxMinValueTuple(numbers);

    // so sánh địa chỉ object
    Console.WriteLine(t == t1); // False
    
    // so sánh giá trị
    Console.WriteLine(t2 == t3); // True
  }
}

Dùng kỹ thuật tham chiếu

Ủa mà khoan, dùng kỹ thuật tham chiếu, ý ông là dùng con trỏ á hả? Thế quái nào trong C# lại có con trỏ? Đừng nói với tôi, ông tính truyền vào địa chỉ của biến sau đó update nó thông qua con trỏ nha, giống giống cách làm của C, C++ ấy. Uấy dà, thật ra cũng tựa tựa như vậy, nhưng không phải chính xác như vậy :v. Thật ra trong C# có cung cấp cho chúng ta các keyword, để giải quyết vấn đề này.

Đó là hai keyword refout, cả hai có thể truyền vào hàm thông qua tham số, riêng ref có thể kết hợp với kiểu trả về để tạo ra cách trả về địa chỉ tham chiếu (này mình sẽ nói ở phần khác). Bây giờ tiếp tục với ví dụ ở trên nhé, chúng ta sẽ giải quyết bài toán một hàm return nhiều giá trị.

Dùng ref

Thêm từ khóa ref vào tham số là được.

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  public static void FindMaxMin(List<int> numbers, ref int max, ref int min)
  {
    max = numbers.Max();
    min = numbers.Min();
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    int max = 0;
    int min = 0;

    FindMaxMin(numbers, ref max, ref min);

    Console.WriteLine($"Max = {max}, Min = {min}"); // Max = 100, Min = -1
  }
}

Dùng out

Bằng cách thêm từ khóa out vào tham số, chúng ta có thể giải quyết được bài toán trên dễ đàng.

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  public static void FindMaxMin(List<int> numbers, out int max, out int min)
  {
    max = numbers.Max();
    min = numbers.Min();
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    int max;
    int min;
    FindMaxMin(numbers, out max, out min);

    Console.WriteLine($"Max = {max}, Min = {min}"); // Max = 100, Min = -1
  }
}

Khác nhau của refout

Ủa rồi giống hệt nhau? Tại sao lại đẻ ra hai thằng cùng 1 mục đích. Why? Thực ra có vài điểm khác nhau, khác nhau cơ bản nhất là khi dùng ref bạn phải init giá trị cho biến trước khi truyền vào, còn với out tuy không cần init giá trị trước khi truyền vào nhưng ở trong hàm thực thi, bạn phải gắn cho out 1 giá trị.

Tóm lại

  • ref yêu cầu biến phải được khởi tạo trước khi truyền vào.
  • out không yêu cầu khởi tạo trước, nhưng phải được gán giá trị trong hàm.

Qua đây chúng ta thấy một điều, để sử dụng return nhiều giá trị cùng lúc, chúng ta nên dùng out hơn là ref, vì nếu quan sát ta có thể thấy rằng ref giống như chúng ta đang binding data 2 chiều với hàm xử lý (init giá trị <-> thay đổi). Còn với out thì chỉ đơn giản thay đổi từ bên trong hàm xử lý (khởi tạo <- thay đổi), chúng ta chỉ việc nhận giá trị đã thay đổi mà thôi.

Dùng phương pháp đen tối (tham chiếu) - (không nên dùng)

Ở đây chúng ta sẽ lợi dụng tính tham chiếu để gán các giá trị cần return vô 1 list object, sau đó từ list object này chúng ta sẽ lấy item con ra. Xem ví dụ bên dưới nha.

using System;
using System.Collections.Generic;
using System.Linq;
          
public class Program
{
  public static void FindMaxMin(List<int> numbers, List<int> kq)
  {
    int max = numbers.Max();
    int min = numbers.Min();
    
    kq.Add(max);
    kq.Add(min);
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var results = new List<int>();
    FindMaxMin(numbers, results);
    
    // results đang trỏ tới vùng nhớ chứ data kiểu List<int>, 
    // trong hàm FindMaxMin nhận được địa chỉ list y hệt vậy, 
    // nên  nếu trong FindMaxMin thực hiện lệnh .Add thực chất đang sửa trên cùng vùng nhớ results trỏ tới
    Console.WriteLine($"Max = {results[0]}, Min = {results[1]}"); // Max = 100, Min = -1
  }
}

Lưu ý: Để ví dụ chơi chơi thì được, chứ cách này không nên dùng tý nào, phần vì nhìn nó chuối lụi, phần vì nó vi phạm một trong bộ nguyên tắc pure function đó là "thay đổi trạng thái bên ngoài phạm vi của nó".

Dùng class object hoặc struct

Ở trong phạm vị section này, mình sẽ không so sánh khác biệt của class và struct nha, mà chúng ta ngầm hiểu mục đích ở đây là như nhau nhé.

Thật ra đây là cách mà chúng ta xài nhiều nhất và hầu như ai cũng đã xài, chẳng qua chừ dô bài viết, mình làm cho nó có vẻ bí ẩn tý mà thôi.

À vì sao cách này hay được xài, và nên xài lúc nào mà không xài mấy cách ở trên? Vâng, nếu dùng cách này thì phải chú ý rằng, chỉ khi nào bạn cần return quá nhiều giá trị về một lúc (tầm 4, 5 cái trở lên), thì lúc này cách này là best, không nói nhiều dùng ngay cho mình.

Cùng vô ví dụ giải quyết bài toán trên nha.

using System;
using System.Collections.Generic;
using System.Linq;

public class MaxMinResult {
  public int Max;
  public int Min;
}

//public struct MaxMinResult {
//	public int Max;
//	public int Min;
//}
          
public class Program
{
  public static MaxMinResult FindMaxMin(List<int> numbers)
  {
    return new MaxMinResult()
    {
      Max = numbers.Max(),
      Min = numbers.Min(),
    };
  }

  public static void Main()
  {
    var numbers = new List<int>{ 1, 10, 100, 99, 44, -1, 33, 99 };
    var result = FindMaxMin(numbers);

    Console.WriteLine($"Max = {result.Max}, Min = {result.Min}"); // Max = 100, Min = -1
  }
}

Thay từ khóa class bằng struct đều được nha.

Tóm lại

Trên đây là một vài cách để giải quyết bài toán return nhiều giá trị cùng lúc trong C#. Tùy vào tình huống, bạn có thể chọn cách tiếp cận phù hợp. Theo quan điểm cá nhân, mình ưu tiên dùng out, ValueTuple, và class/struct.

Hy vọng bài viết hữu ích. Nếu thấy hay, hãy like, share và bookmark nhé. Cảm ơn bạn đã đọc!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí