Cách tốt nhất để kết hợp hai hoặc nhiều mảng byte trong C #


238

Tôi có 3 mảng byte trong C # mà tôi cần kết hợp thành một. Điều gì sẽ là phương pháp hiệu quả nhất để hoàn thành nhiệm vụ này?


3
Yêu cầu cụ thể của bạn là gì? Bạn có đang kết hợp các mảng hoặc bạn đang bảo tồn nhiều phiên bản của cùng một giá trị? Bạn có muốn các mục được sắp xếp, hoặc bạn muốn duy trì thứ tự trong các mảng ban đầu? Bạn đang tìm kiếm hiệu quả về tốc độ hoặc trong các dòng mã?
jason

Yêu nó, "tốt nhất" phụ thuộc vào yêu cầu của bạn là gì.
Ady

7
Nếu bạn có thể sử dụng LINQ, thì bạn chỉ có thể sử dụng Concatphương thức:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
casperOne

1
Hãy cố gắng để rõ ràng hơn trong câu hỏi của bạn. Câu hỏi mơ hồ này đã gây ra nhiều nhầm lẫn giữa những người đủ tốt để dành thời gian trả lời bạn.
Drew Noakes

Câu trả lời:


325

Đối với các kiểu nguyên thủy (bao gồm byte), sử dụng System.Buffer.BlockCopythay vìSystem.Array.Copy . Nó nhanh hơn.

Tôi đã tính thời gian cho mỗi phương thức được đề xuất trong một vòng lặp được thực hiện 1 triệu lần bằng cách sử dụng 3 mảng 10 byte mỗi phương thức. Đây là kết quả:

  1. Mảng Byte mới sử dụng System.Array.Copy - 0,2187556 giây
  2. Mảng Byte mới sử dụng System.Buffer.BlockCopy - 0.1406286 giây
  3. Vô số <byte> sử dụng toán tử năng suất C # - 0,0781270 giây
  4. Vô số <byte> sử dụng concat của LINQ <> - 0,0781270 giây

Tôi đã tăng kích thước của mỗi mảng lên 100 phần tử và chạy lại bài kiểm tra:

  1. Mảng Byte mới sử dụng System.Array.Copy - 0,2812554 giây
  2. Mảng Byte mới sử dụng System.Buffer.BlockCopy - 0,2500048 giây
  3. Vô số <byte> sử dụng toán tử năng suất C # - 0,0625012 giây
  4. Vô số <byte> sử dụng concat của LINQ <> - 0,0781265 giây

Tôi đã tăng kích thước của mỗi mảng lên 1000 phần tử và chạy lại bài kiểm tra:

  1. Mảng Byte mới sử dụng System.Array.Copy - 1.0781457 giây
  2. Mảng Byte mới sử dụng System.Buffer.BlockCopy - 1.0156445 giây
  3. Vô số <byte> sử dụng toán tử năng suất C # - 0,0625012 giây
  4. Vô số <byte> sử dụng concat của LINQ <> - 0,0781265 giây

Cuối cùng, tôi đã tăng kích thước của mỗi mảng lên 1 triệu phần tử và chạy lại thử nghiệm, thực hiện mỗi vòng lặp chỉ 4000 lần:

  1. Mảng Byte mới sử dụng System.Array.Copy - 13,4533833 giây
  2. Mảng Byte mới sử dụng System.Buffer.BlockCopy - 13.1096267 giây
  3. Vô số <byte> sử dụng toán tử năng suất C # - 0 giây
  4. Có thể đếm được <byte> bằng cách sử dụng concat của LINQ <> - 0 giây

Vì vậy, nếu bạn cần một mảng byte mới, hãy sử dụng

byte[] rv = new byte[a1.Length + a2.Length + a3.Length];
System.Buffer.BlockCopy(a1, 0, rv, 0, a1.Length);
System.Buffer.BlockCopy(a2, 0, rv, a1.Length, a2.Length);
System.Buffer.BlockCopy(a3, 0, rv, a1.Length + a2.Length, a3.Length);

Nhưng, nếu bạn có thể sử dụng một phương thức IEnumerable<byte>, DEFINITELY thích phương thức concat <> của LINQ. Nó chỉ chậm hơn một chút so với toán tử năng suất C #, nhưng ngắn gọn và thanh lịch hơn.

IEnumerable<byte> rv = a1.Concat(a2).Concat(a3);

Nếu bạn có số lượng mảng tùy ý và đang sử dụng .NET 3.5, bạn có thể làm cho System.Buffer.BlockCopygiải pháp chung chung hơn như thế này:

private byte[] Combine(params byte[][] arrays)
{
    byte[] rv = new byte[arrays.Sum(a => a.Length)];
    int offset = 0;
    foreach (byte[] array in arrays) {
        System.Buffer.BlockCopy(array, 0, rv, offset, array.Length);
        offset += array.Length;
    }
    return rv;
}

* Lưu ý: Khối trên yêu cầu bạn thêm không gian tên sau ở trên cùng để nó hoạt động.

using System.Linq;

Theo quan điểm của Jon Skeet về việc lặp lại các cấu trúc dữ liệu tiếp theo (mảng byte so với IEnumerable <byte>), tôi đã chạy lại thử nghiệm thời gian cuối cùng (1 triệu phần tử, 4000 lần lặp), thêm một vòng lặp lặp lại trên toàn bộ mảng vượt qua:

  1. Mảng Byte mới sử dụng System.Array.Copy - 78.20550510 giây
  2. Mảng Byte mới sử dụng System.Buffer.BlockCopy - 77,89261900 giây
  3. Vô số <byte> sử dụng toán tử năng suất C # - 551.7150161 giây
  4. Có thể đếm được <byte> bằng cách sử dụng concat của LINQ <> - 448.1804799 giây

Vấn đề là, RẤT quan trọng để hiểu hiệu quả của cả việc tạo và sử dụng cấu trúc dữ liệu kết quả. Chỉ cần tập trung vào hiệu quả của việc tạo có thể bỏ qua sự không hiệu quả liên quan đến việc sử dụng. Kudos, Jon.


61
Nhưng bạn có thực sự chuyển đổi nó thành một mảng ở cuối, như câu hỏi yêu cầu không? Nếu không, tất nhiên là nhanh hơn - nhưng nó không đáp ứng các yêu cầu.
Jon Skeet

18
Re: Matt Davis - Không thành vấn đề nếu "yêu cầu" của bạn cần biến IEnumerable thành một mảng - tất cả những gì bạn yêu cầu là kết quả thực sự được sử dụng trong một số lỗi . Lý do kiểm tra hiệu suất của bạn trên IEnumerable rất thấp là vì bạn không thực sự làm gì cả ! LINQ không thực hiện bất kỳ công việc nào cho đến khi bạn cố gắng sử dụng kết quả. Vì lý do này, tôi thấy câu trả lời của bạn không chính xác và có thể khiến người khác sử dụng LINQ khi họ hoàn toàn không nên nếu họ quan tâm đến hiệu suất.
csauve

12
Tôi đọc toàn bộ câu trả lời bao gồm cập nhật của bạn, bình luận của tôi đứng. Tôi biết rằng tôi tham gia bữa tiệc muộn, nhưng câu trả lời rất sai lệch và nửa đầu là sai một cách rõ ràng .
csauve

14
Tại sao câu trả lời chứa thông tin sai lệch và sai lệch là câu trả lời được bình chọn hàng đầu và được chỉnh sửa về cơ bản hoàn toàn vô hiệu hóa tuyên bố ban đầu của nó sau khi ai đó (Jon Skeet) chỉ ra rằng nó thậm chí không trả lời câu hỏi của OP?
MrCC

3
Câu trả lời sai lệch. Ngay cả phiên bản không trả lời câu hỏi.
Serge Profafilecebook

154

Nhiều câu trả lời dường như đang bỏ qua các yêu cầu đã nêu:

  • Kết quả phải là một mảng byte
  • Nó nên hiệu quả nhất có thể

Cả hai cùng loại trừ một chuỗi byte LINQ - bất cứ điều gì yieldsẽ làm cho không thể có được kích thước cuối cùng mà không lặp lại trong toàn bộ chuỗi.

Nếu đó không phải là những yêu cầu thực sự của khóa học, LINQ có thể là một giải pháp hoàn toàn tốt (hoặc việc IList<T>thực hiện). Tuy nhiên, tôi sẽ cho rằng Superdumbell biết anh ta muốn gì.

(EDIT: Tôi vừa có một suy nghĩ khác. Có một sự khác biệt lớn về ngữ nghĩa giữa việc tạo một bản sao của các mảng và đọc chúng một cách lười biếng. Hãy xem xét điều gì xảy ra nếu bạn thay đổi dữ liệu trong một trong các mảng "nguồn" sau khi gọi Combine (hoặc bất cứ điều gì ) phương pháp nhưng trước khi sử dụng kết quả - với đánh giá lười biếng, sự thay đổi đó sẽ hiển thị. Với một bản sao ngay lập tức, nó sẽ không xảy ra. Các tình huống khác nhau sẽ đòi hỏi hành vi khác nhau - chỉ cần một cái gì đó để nhận biết.)

Dưới đây là các phương pháp được đề xuất của tôi - rất giống với các phương pháp có trong một số câu trả lời khác, chắc chắn :)

public static byte[] Combine(byte[] first, byte[] second)
{
    byte[] ret = new byte[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static byte[] Combine(byte[] first, byte[] second, byte[] third)
{
    byte[] ret = new byte[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static byte[] Combine(params byte[][] arrays)
{
    byte[] ret = new byte[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (byte[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}

Tất nhiên, phiên bản "params" yêu cầu tạo ra một mảng các mảng byte trước, điều này dẫn đến sự kém hiệu quả.


Jon, tôi hiểu chính xác những gì bạn đang nói. Điểm duy nhất của tôi là đôi khi các câu hỏi được đặt ra với một triển khai cụ thể đã có trong đầu mà không nhận ra rằng các giải pháp khác tồn tại. Đơn giản chỉ cần cung cấp một câu trả lời mà không đưa ra giải pháp thay thế có vẻ như là một sự bất đồng với tôi. Suy nghĩ?
Matt Davis

1
@Matt: Vâng, cung cấp các lựa chọn thay thế là tốt - nhưng đáng để giải thích rằng chúng các lựa chọn thay vì bỏ qua như là câu trả lời cho câu hỏi đang được hỏi. (Tôi không nói rằng bạn đã làm điều đó - câu trả lời của bạn rất hay.)
Jon Skeet

4
(Mặc dù tôi nghĩ rằng điểm chuẩn hiệu suất của bạn cũng sẽ hiển thị thời gian thực hiện tất cả các kết quả trong từng trường hợp, để tránh đánh giá lười biếng một lợi thế không công bằng.)
Jon Skeet

1
Ngay cả khi không đáp ứng yêu cầu "kết quả phải là một mảng", chỉ cần đáp ứng yêu cầu "kết quả phải được sử dụng trong một số lỗi" sẽ làm cho LINQ không tối ưu. Tôi nghĩ rằng yêu cầu để có thể sử dụng kết quả nên được ngầm định!
csauve

2
@andleer: Ngoài mọi thứ khác, Buffer.BlockCopy chỉ hoạt động với các kiểu nguyên thủy.
Jon Skeet

44

Tôi lấy ví dụ LINQ của Matt một bước nữa để làm sạch mã:

byte[] rv = a1.Concat(a2).Concat(a3).ToArray();

Trong trường hợp của tôi, các mảng là nhỏ, vì vậy tôi không quan tâm đến hiệu suất.


3
Giải pháp ngắn gọn và đơn giản, một bài kiểm tra hiệu suất sẽ là tuyệt vời!
Sebastian

3
Điều này chắc chắn rõ ràng, dễ đọc, không yêu cầu thư viện / người trợ giúp bên ngoài, và, về mặt thời gian phát triển, khá hiệu quả. Tuyệt vời khi hiệu suất thời gian chạy không quan trọng.
biley

28

Nếu bạn chỉ cần một mảng byte mới, thì hãy sử dụng như sau:

byte[] Combine(byte[] a1, byte[] a2, byte[] a3)
{
    byte[] ret = new byte[a1.Length + a2.Length + a3.Length];
    Array.Copy(a1, 0, ret, 0, a1.Length);
    Array.Copy(a2, 0, ret, a1.Length, a2.Length);
    Array.Copy(a3, 0, ret, a1.Length + a2.Length, a3.Length);
    return ret;
}

Ngoài ra, nếu bạn chỉ cần một IEnumerable duy nhất, hãy xem xét sử dụng toán tử năng suất C # 2.0:

IEnumerable<byte> Combine(byte[] a1, byte[] a2, byte[] a3)
{
    foreach (byte b in a1)
        yield return b;
    foreach (byte b in a2)
        yield return b;
    foreach (byte b in a3)
        yield return b;
}

Tôi đã làm một cái gì đó tương tự như tùy chọn thứ 2 của bạn để hợp nhất các luồng lớn, hoạt động như một bùa mê. :)
Greg D

2
Tùy chọn thứ hai là tuyệt vời. +1.
R. Martinho Fernandes

10

Tôi thực sự gặp phải một số vấn đề khi sử dụng Concat ... (với các mảng trong 10 triệu, nó thực sự đã bị sập).

Tôi thấy những điều sau đây đơn giản, dễ dàng và hoạt động đủ tốt mà không gặp sự cố với tôi và nó hoạt động với BẤT K Array số mảng nào (không chỉ ba) (Nó sử dụng LINQ):

public static byte[] ConcatByteArrays(params byte[][]  arrays)
{
    return arrays.SelectMany(x => x).ToArray();
}

6

Lớp bộ nhớ thực hiện công việc này khá độc đáo đối với tôi. Tôi không thể có được lớp đệm để chạy nhanh như bộ nhớ.

using (MemoryStream ms = new MemoryStream())
{
  ms.Write(BitConverter.GetBytes(22),0,4);
  ms.Write(BitConverter.GetBytes(44),0,4);
  ms.ToArray();
}

3
Như qwe đã nói, tôi đã thực hiện một bài kiểm tra trong vòng lặp 10.000.000 lần và MemoryStream đạt được 290% SLOWER so với Buffer.BlockCopy
esac

Trong một số trường hợp, bạn có thể lặp đi lặp lại vô số mảng mà không có bất kỳ sự biết trước nào về độ dài mảng riêng lẻ. Điều này hoạt động tốt trong kịch bản này. BlockCopy dựa vào việc có một mảng đích được xử lý trước
Sentinel

Như @Sentinel đã nói, câu trả lời này rất phù hợp với tôi vì tôi không có kiến ​​thức về kích thước của những thứ tôi phải viết và cho phép tôi làm mọi thứ rất sạch sẽ. Nó cũng chơi tốt với Span <ReadOnly] của .NET Core 3!
Nước

Nếu bạn khởi tạo MemoryStream với kích thước cuối cùng của kích thước thì nó sẽ không được tạo lại và nó sẽ nhanh hơn @esac.
Tono Nam

2
    public static bool MyConcat<T>(ref T[] base_arr, ref T[] add_arr)
    {
        try
        {
            int base_size = base_arr.Length;
            int size_T = System.Runtime.InteropServices.Marshal.SizeOf(base_arr[0]);
            Array.Resize(ref base_arr, base_size + add_arr.Length);
            Buffer.BlockCopy(add_arr, 0, base_arr, base_size * size_T, add_arr.Length * size_T);
        }
        catch (IndexOutOfRangeException ioor)
        {
            MessageBox.Show(ioor.Message);
            return false;
        }
        return true;
    }

Thật không may, điều này sẽ không làm việc với tất cả các loại. Marshal.SizeOf () sẽ không thể trả lại kích thước cho nhiều loại (thử sử dụng phương thức này với các chuỗi chuỗi và bạn sẽ thấy ngoại lệ "Loại 'System.String' không thể được sắp xếp thành một cấu trúc không được quản lý; offset có thể được tính toán ". Bạn có thể thử giới hạn tham số loại chỉ với các loại tham chiếu (bằng cách thêm where T : struct), nhưng - không phải là chuyên gia trong các bộ phận của CLR - Tôi không thể nói liệu bạn có thể có ngoại lệ đối với các cấu trúc nhất định không (ví dụ: nếu chúng chứa các trường loại tham chiếu).
Daniel Scott

2
    public static byte[] Concat(params byte[][] arrays) {
        using (var mem = new MemoryStream(arrays.Sum(a => a.Length))) {
            foreach (var array in arrays) {
                mem.Write(array, 0, array.Length);
            }
            return mem.ToArray();
        }
    }

Câu trả lời của bạn có thể tốt hơn nếu bạn đã đăng một lời giải thích nhỏ về mẫu mã này.
Thu hút

1
nó nối một mảng các mảng byte thành một mảng byte lớn (như thế này): [1,2,3] + [4,5] + [6,7] ==> [1,2,3,4,5 , 6,7]
Peter Ertl

1

Có thể sử dụng thuốc generic để kết hợp mảng. Mã sau có thể dễ dàng được mở rộng thành ba mảng. Bằng cách này, bạn không bao giờ cần phải sao chép mã cho các loại mảng khác nhau. Một số câu trả lời ở trên có vẻ quá phức tạp đối với tôi.

private static T[] CombineTwoArrays<T>(T[] a1, T[] a2)
    {
        T[] arrayCombined = new T[a1.Length + a2.Length];
        Array.Copy(a1, 0, arrayCombined, 0, a1.Length);
        Array.Copy(a2, 0, arrayCombined, a1.Length, a2.Length);
        return arrayCombined;
    }

0

Đây là một khái quát về câu trả lời được cung cấp bởi @Jon Skeet. Về cơ bản là giống nhau, chỉ có nó có thể sử dụng được cho bất kỳ loại mảng nào, không chỉ các byte:

public static T[] Combine<T>(T[] first, T[] second)
{
    T[] ret = new T[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static T[] Combine<T>(T[] first, T[] second, T[] third)
{
    T[] ret = new T[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static T[] Combine<T>(params T[][] arrays)
{
    T[] ret = new T[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (T[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}

3
NGUY HIỂM! Các phương thức này sẽ không hoạt động thuộc tính với bất kỳ loại mảng nào có các phần tử dài hơn một byte (gần như mọi thứ khác với mảng byte). Buffer.BlockCopy () hoạt động với số lượng byte, không phải số lượng phần tử mảng. Lý do nó có thể được sử dụng dễ dàng với một mảng byte là vì mọi phần tử của mảng là một byte đơn, do đó độ dài vật lý của mảng bằng với số lượng phần tử. Để biến các phương thức byte [] của John thành các phương thức chung, bạn sẽ cần nhiều độ lệch và độ dài theo độ dài byte của một phần tử mảng duy nhất - nếu không bạn sẽ không sao chép tất cả dữ liệu.
Daniel Scott

2
Thông thường để thực hiện công việc này, bạn sẽ tính kích thước của một phần tử bằng cách sử dụng sizeof(...)và nhân số đó với số phần tử bạn muốn sao chép, nhưng sizeof không thể được sử dụng với một loại chung. Có thể - đối với một số loại - để sử dụng Marshal.SizeOf(typeof(T)), nhưng bạn sẽ gặp lỗi thời gian chạy với một số loại nhất định (ví dụ: chuỗi). Ai đó có kiến ​​thức kỹ lưỡng hơn về hoạt động bên trong của các loại CLR sẽ có thể chỉ ra tất cả các bẫy có thể có ở đây. Đủ để nói rằng viết một phương pháp nối mảng chung [sử dụng BlockCopy] không phải là chuyện nhỏ.
Daniel Scott

2
Và cuối cùng - bạn có thể viết một phương thức nối mảng chung như thế này gần như chính xác theo cách hiển thị ở trên (với hiệu suất thấp hơn một chút) bằng cách sử dụng Array.Copy thay thế. Chỉ cần thay thế tất cả các cuộc gọi Buffer.BlockCopy bằng các cuộc gọi Array.Copy.
Daniel Scott

0
    /// <summary>
    /// Combine two Arrays with offset and count
    /// </summary>
    /// <param name="src1"></param>
    /// <param name="offset1"></param>
    /// <param name="count1"></param>
    /// <param name="src2"></param>
    /// <param name="offset2"></param>
    /// <param name="count2"></param>
    /// <returns></returns>
    public static T[] Combine<T>(this T[] src1, int offset1, int count1, T[] src2, int offset2, int count2) 
        => Enumerable.Range(0, count1 + count2).Select(a => (a < count1) ? src1[offset1 + a] : src2[offset2 + a - count1]).ToArray();

Cảm ơn bạn đã đóng góp. Vì đã có một số câu trả lời được đánh giá cao về vấn đề này từ hơn một thập kỷ trước, sẽ rất hữu ích khi đưa ra lời giải thích về những gì phân biệt cách tiếp cận của bạn. Tại sao ai đó nên sử dụng điều này thay vì ví dụ như câu trả lời được chấp nhận?
Jeremy Caney

Tôi thích sử dụng các phương thức mở rộng, bởi vì có mã rõ ràng để hiểu. Mã này chọn hai mảng với chỉ số bắt đầu và đếm và concat. Ngoài ra phương pháp này mở rộng. Vì vậy, đây là cho tất cả các loại mảng sẵn sàng cho mọi thời điểm
Mehmet NLÜ

Điều đó có ý nghĩa tốt với tôi! Bạn có phiền chỉnh sửa câu hỏi của bạn để bao gồm thông tin đó? Tôi nghĩ rằng sẽ rất có giá trị cho những độc giả tương lai có được sự thẳng thắn đó, vì vậy họ có thể nhanh chóng phân biệt cách tiếp cận của bạn với các câu trả lời hiện có. Cảm ơn bạn!
Jeremy Caney

-1

Tất cả những gì bạn cần để vượt qua danh sách Mảng Byte và hàm này sẽ trả về cho bạn Mảng Byte (Sáp nhập). Đây là giải pháp tốt nhất tôi nghĩ :).

public static byte[] CombineMultipleByteArrays(List<byte[]> lstByteArray)
        {
            using (var ms = new MemoryStream())
            {
                using (var doc = new iTextSharp.text.Document())
                {
                    using (var copy = new PdfSmartCopy(doc, ms))
                    {
                        doc.Open();
                        foreach (var p in lstByteArray)
                        {
                            using (var reader = new PdfReader(p))
                            {
                                copy.AddDocument(reader);
                            }
                        }

                        doc.Close();
                    }
                }
                return ms.ToArray();
            }
        }

-5

Concat là câu trả lời đúng, nhưng vì một số lý do, một thứ được kiểm soát sẽ nhận được nhiều phiếu bầu nhất. Nếu bạn thích câu trả lời đó, có lẽ bạn sẽ thích giải pháp tổng quát hơn này hơn nữa:

    IEnumerable<byte> Combine(params byte[][] arrays)
    {
        foreach (byte[] a in arrays)
            foreach (byte b in a)
                yield return b;
    }

Điều này sẽ cho phép bạn làm những việc như:

    byte[] c = Combine(new byte[] { 0, 1, 2 }, new byte[] { 3, 4, 5 }).ToArray();

5
Câu hỏi đặc biệt yêu cầu giải pháp hiệu quả nhất. Có thể đếm được.ToArray sẽ không hiệu quả lắm, vì nó không thể biết kích thước của mảng cuối cùng để bắt đầu - trong khi các kỹ thuật cuộn bằng tay có thể.
Jon Skeet
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.