Nhược điểm của các loại bất biến là gì?


12

Tôi thấy bản thân mình sử dụng nhiều loại bất biến hơn khi các thể hiện của lớp dự kiến ​​sẽ không bị thay đổi . Nó đòi hỏi nhiều công việc hơn (xem ví dụ bên dưới), nhưng giúp sử dụng các loại trong môi trường đa luồng dễ dàng hơn.

Đồng thời, tôi hiếm khi thấy các loại bất biến trong các ứng dụng khác, ngay cả khi khả năng biến đổi sẽ không mang lại lợi ích cho bất kỳ ai.

Câu hỏi: Tại sao các loại bất biến rất hiếm khi được sử dụng trong các ứng dụng khác?

  • Đây có phải là vì nó dài hơn để viết mã cho một loại không thay đổi,
  • Hoặc tôi đang thiếu một cái gì đó và có một số nhược điểm quan trọng khi sử dụng các loại không thay đổi?

Ví dụ từ cuộc sống thực

Giả sử bạn nhận được Weathertừ API RESTful như thế:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

Những gì chúng ta thường thấy là (các dòng và nhận xét mới được xóa để rút ngắn mã):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

Mặt khác, tôi sẽ viết nó theo cách này, với điều kiện nhận được Weathertừ API, sau đó sửa đổi nó sẽ là lạ: thay đổi Temperaturehoặc Skysẽ không thay đổi thời tiết trong thế giới thực và thay đổi CorrespondingCitycũng không có ý nghĩa gì.

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}

3
Đây là một công việc đòi hỏi nhiều hơn nữa. Theo kinh nghiệm của tôi, nó đòi hỏi ít công việc hơn .
Konrad Rudolph

1
@KonradRudolph: bằng cách làm việc nhiều hơn , ý tôi là viết nhiều mã hơn để tạo ra một lớp bất biến. Ví dụ từ câu hỏi của tôi minh họa điều này, với 7 dòng cho một lớp có thể thay đổi và 19 dòng cho một lớp bất biến.
Arseni Mourzenko

Bạn có thể giảm việc nhập mã bằng cách sử dụng tính năng Code Snippets trong Visual Studio nếu bạn đang sử dụng nó. Bạn có thể tạo các đoạn tùy chỉnh của mình và để IDE xác định cho bạn trường và thuộc tính cùng một lúc với một vài khóa. Các loại không thay đổi rất cần thiết cho đa luồng và được sử dụng rộng rãi trong các ngôn ngữ như Scala.
Mert Akcakaya

@Mert: Đoạn mã rất tốt cho những điều đơn giản. Viết một đoạn mã sẽ xây dựng một lớp đầy đủ với các nhận xét về các trường và thuộc tính và sắp xếp đúng sẽ không phải là một nhiệm vụ dễ dàng.
Arseni Mourzenko

5
Tôi không đồng ý với ví dụ được đưa ra, phiên bản bất biến đang làm nhiều thứ khác nhau. Bạn có thể loại bỏ các biến cấp độ cá thể bằng cách khai báo các thuộc tính với các hàm truy cập {get; private set;}và thậm chí biến có thể thay đổi phải có hàm tạo, bởi vì tất cả các trường đó phải luôn được đặt và tại sao bạn không thực thi điều đó? Làm cho hai thay đổi hoàn toàn hợp lý đó mang lại cho chúng tính năng và ngang giá LoC.
Phoshi

Câu trả lời:


16

Tôi lập trình trong C # và Objective-C. Tôi thực sự thích gõ không thay đổi, nhưng trong cuộc sống thực, tôi luôn bị buộc phải hạn chế sử dụng nó, chủ yếu cho các loại dữ liệu, vì những lý do sau:

  1. Nỗ lực thực hiện so với các loại đột biến. Với một loại bất biến, bạn sẽ cần phải có một hàm tạo yêu cầu đối số cho tất cả các thuộc tính. Ví dụ của bạn là một trong những tốt. Hãy thử tưởng tượng rằng bạn có 10 lớp, mỗi lớp có 5-10 thuộc tính. Để làm cho mọi thứ dễ dàng hơn, bạn có thể cần phải có một lớp trình xây dựng để xây dựng hoặc tạo các thể hiện bất biến được sửa đổi theo cách tương tự StringBuilderhoặc UriBuildertrong C #, hoặc WeatherBuildertrong trường hợp của bạn. Đây là lý do chính đối với tôi vì nhiều lớp tôi thiết kế không đáng để nỗ lực như vậy.
  2. Khả năng sử dụng của người tiêu dùng . Một loại bất biến khó sử dụng hơn so với loại đột biến. Khởi tạo yêu cầu khởi tạo tất cả các giá trị. Tính không thay đổi cũng có nghĩa là chúng ta không thể chuyển thể hiện sang một phương thức để sửa đổi giá trị của nó mà không cần sử dụng trình xây dựng và nếu chúng ta cần phải có một trình xây dựng thì nhược điểm nằm ở (1).
  3. Khả năng tương thích với khung ngôn ngữ. Nhiều khung dữ liệu yêu cầu các kiểu có thể thay đổi và các hàm tạo mặc định để hoạt động. Ví dụ: bạn không thể thực hiện truy vấn LINQ-to-SQL lồng nhau với các loại không thay đổi và bạn không thể liên kết các thuộc tính để được chỉnh sửa trong các trình soạn thảo như TextBox của Windows Forms.

Nói tóm lại, tính bất biến là tốt cho các đối tượng hành xử giống như các giá trị hoặc chỉ có một vài thuộc tính. Trước khi làm bất cứ điều gì bất biến, bạn phải xem xét nỗ lực cần thiết và khả năng sử dụng của chính lớp đó sau khi biến nó thành bất biến.


11
"Việc khởi tạo cần tất cả các giá trị trước đó.": Cũng là một loại có thể thay đổi, trừ khi bạn chấp nhận rủi ro có một đối tượng với các trường chưa được khởi tạo trôi nổi xung quanh ...
Giorgio

@Giorgio Đối với loại có thể thay đổi, hàm tạo mặc định sẽ khởi tạo thể hiện về trạng thái mặc định và trạng thái của thể hiện có thể được thay đổi sau khi khởi tạo.
tia

8
Đối với loại không thay đổi, bạn có thể có cùng hàm tạo mặc định và tạo bản sao sau bằng cách sử dụng hàm tạo khác. Nếu các giá trị mặc định là hợp lệ cho loại có thể thay đổi, thì chúng cũng hợp lệ cho loại không thay đổi vì trong cả hai trường hợp, bạn đang tạo mô hình cho cùng một thực thể. Hay sự khác biệt là gì?
Giorgio

1
Một điều nữa để xem xét là những gì loại đại diện. Hợp đồng dữ liệu không tạo ra các loại bất biến tốt vì tất cả các điểm này, nhưng các loại dịch vụ được khởi tạo với phụ thuộc hoặc chỉ đọc dữ liệu và sau đó thực hiện các thao tác là tuyệt vời vì tính bất biến vì các hoạt động sẽ hoạt động ổn định và trạng thái của dịch vụ không thể Thay đổi rủi ro đó.
Kevin

1
Tôi hiện đang viết mã trong F #, trong đó tính bất biến là mặc định (do đó dễ thực hiện hơn). Tôi thấy rằng điểm 3 của bạn là trở ngại lớn. Ngay khi bạn sử dụng hầu hết các thư viện .Net tiêu chuẩn, bạn sẽ cần phải nhảy qua các vòng. (Và nếu họ sử dụng sự phản chiếu và do đó bỏ qua sự bất biến ... argh!)
Guran

5

Các loại nói chung không thay đổi được tạo bằng các ngôn ngữ không xoay quanh tính bất biến sẽ có xu hướng tốn nhiều thời gian của nhà phát triển hơn cũng như có khả năng sử dụng nếu chúng yêu cầu một số loại đối tượng "trình tạo" để thể hiện các thay đổi mong muốn (điều này không có nghĩa là tổng thể công việc sẽ nhiều hơn, nhưng có một chi phí trả trước trong những trường hợp này). Ngoài ra, bất kể ngôn ngữ có giúp dễ dàng tạo các loại bất biến hay không, nó sẽ có xu hướng luôn yêu cầu một số chi phí xử lý và bộ nhớ cho các loại dữ liệu không tầm thường.

Làm cho các chức năng không có tác dụng phụ

Nếu bạn đang làm việc trong các ngôn ngữ không xoay quanh tính bất biến, thì tôi nghĩ rằng phương pháp thực dụng không phải là tìm cách làm cho mọi loại dữ liệu đơn lẻ trở nên bất biến. Một tư duy có năng suất cao hơn nhiều mang lại cho bạn nhiều lợi ích tương tự là tập trung vào tối đa hóa số lượng chức năng trong hệ thống của bạn mà không gây ra tác dụng phụ .

Một ví dụ đơn giản, nếu bạn có một chức năng gây ra tác dụng phụ như thế này:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Sau đó, chúng ta không cần một kiểu dữ liệu số nguyên bất biến cấm các toán tử như gán sau khởi tạo để làm cho hàm đó tránh được các tác dụng phụ. Chúng ta chỉ đơn giản có thể làm điều này:

// Returns the absolute value of 'x'.
int abs(int x);

Bây giờ chức năng không gây rối với xhoặc bất cứ điều gì ngoài phạm vi của nó, và trong trường hợp tầm thường này, chúng tôi thậm chí có thể đã cạo một số chu kỳ bằng cách tránh bất kỳ chi phí nào liên quan đến việc xác định / răng cưa. Ít nhất thì phiên bản thứ hai không nên đắt hơn tính toán so với phiên bản đầu tiên.

Những thứ đắt tiền để sao chép đầy đủ

Tất nhiên hầu hết các trường hợp không phải là tầm thường này nếu chúng ta muốn tránh làm cho một chức năng gây ra tác dụng phụ. Một trường hợp sử dụng trong thế giới thực phức tạp có thể giống như thế này:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

Tại thời điểm đó, lưới có thể cần vài trăm megabyte bộ nhớ với hơn một trăm nghìn đa giác, thậm chí nhiều đỉnh và cạnh hơn, nhiều bản đồ kết cấu, mục tiêu hình thái, v.v ... Sẽ rất tốn kém khi sao chép toàn bộ lưới chỉ để thực hiện điều này transformChức năng không có tác dụng phụ, như vậy:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

Và trong những trường hợp này, việc sao chép toàn bộ một cái gì đó thường sẽ là một chi phí sử thi mà tôi thấy hữu ích khi biến Meshthành một cấu trúc dữ liệu bền vững và một loại bất biến với "trình xây dựng" tương tự để tạo ra các phiên bản sửa đổi của nó để nó được sửa đổi có thể chỉ đơn giản là bản sao nông và phần tham chiếu không phải là duy nhất. Đó là tất cả với trọng tâm là có thể viết các hàm lưới không có tác dụng phụ.

Cấu trúc dữ liệu liên tục

Và trong những trường hợp sao chép mọi thứ rất tốn kém, tôi đã thấy nỗ lực của việc thiết kế một thứ bất biến Meshđể thực sự được đền đáp mặc dù nó có chi phí hơi cao, vì nó không đơn giản hóa sự an toàn của luồng. Nó cũng đơn giản hóa việc chỉnh sửa không phá hủy (cho phép người dùng tạo lớp hoạt động của lưới mà không sửa đổi bản sao gốc của mình), hoàn tác các hệ thống (giờ đây hệ thống hoàn tác chỉ có thể lưu trữ một bản sao bất biến của lưới trước những thay đổi được thực hiện bởi một thao tác mà không làm nổ bộ nhớ sử dụng) và an toàn ngoại lệ (hiện tại nếu có ngoại lệ xảy ra trong chức năng trên, chức năng này không phải quay lại và hoàn tác tất cả các tác dụng phụ của nó vì nó không gây ra bất kỳ sự khởi đầu nào).

Tôi có thể tự tin nói rằng trong những trường hợp này, thời gian cần thiết để làm cho các cấu trúc dữ liệu khổng lồ này tiết kiệm được nhiều thời gian hơn so với chi phí, vì tôi đã so sánh chi phí bảo trì của các thiết kế mới này so với các thiết kế cũ xoay quanh khả năng biến đổi và chức năng gây ra tác dụng phụ, và các thiết kế đột biến trước đây tốn nhiều thời gian hơn và dễ bị lỗi của con người hơn, đặc biệt là trong các lĩnh vực thực sự hấp dẫn các nhà phát triển bỏ bê trong thời gian khủng hoảng, như ngoại lệ - an toàn.

Vì vậy, tôi nghĩ rằng các loại dữ liệu bất biến thực sự có hiệu quả trong những trường hợp này, nhưng không phải mọi thứ đều phải được làm bất biến để làm cho phần lớn các chức năng trong hệ thống của bạn không có tác dụng phụ. Nhiều thứ đủ rẻ để chỉ cần sao chép đầy đủ. Ngoài ra, nhiều ứng dụng trong thế giới thực sẽ cần phải gây ra một số tác dụng phụ ở đây và đó (ít nhất là giống như lưu tệp), nhưng thông thường có nhiều chức năng hơn có thể không có tác dụng phụ.

Quan điểm của việc có một số loại dữ liệu bất biến đối với tôi là đảm bảo rằng chúng ta có thể viết số lượng hàm tối đa để không bị tác dụng phụ mà không phải chịu chi phí sử thi dưới dạng sao chép sâu các cấu trúc dữ liệu khổng lồ sang trái và phải khi chỉ có các phần nhỏ trong số họ cần phải được sửa đổi. Có các cấu trúc dữ liệu liên tục trong các trường hợp đó sau đó trở thành một chi tiết tối ưu hóa để cho phép chúng tôi viết các chức năng của mình để không có tác dụng phụ mà không phải trả chi phí sử thi để làm việc đó.

Chi phí bất biến

Bây giờ về mặt khái niệm các phiên bản có thể thay đổi sẽ luôn có lợi thế về hiệu quả. Luôn luôn có chi phí tính toán liên quan đến cấu trúc dữ liệu bất biến. Nhưng tôi thấy đó là một sự trao đổi xứng đáng trong các trường hợp tôi đã mô tả ở trên, và bạn có thể tập trung vào việc làm cho chi phí đủ tối thiểu trong tự nhiên. Tôi thích kiểu tiếp cận đó trong đó tính chính xác trở nên dễ dàng và tối ưu hóa trở nên khó hơn thay vì tối ưu hóa dễ hơn nhưng tính chính xác trở nên khó hơn. Nó gần như không làm mất uy tín để có mã hoạt động hoàn toàn chính xác khi cần thêm một số điều chỉnh mã không hoạt động chính xác ngay từ đầu cho dù nó có đạt được kết quả không chính xác nhanh như thế nào.


3

Hạn chế duy nhất tôi có thể nghĩ đến là về mặt lý thuyết, việc sử dụng dữ liệu bất biến có thể chậm hơn dữ liệu có thể thay đổi - việc tạo một thể hiện mới và thu thập dữ liệu trước đó chậm hơn so với sửa đổi dữ liệu hiện có.

"Vấn đề" khác là bạn không thể chỉ sử dụng các loại bất biến. Cuối cùng, bạn phải mô tả trạng thái và bạn phải sử dụng các loại có thể thay đổi để làm như vậy - mà không thay đổi trạng thái, bạn không thể thực hiện bất kỳ công việc nào.

Nhưng quy tắc chung vẫn là sử dụng các loại bất biến bất cứ nơi nào bạn có thể và làm cho các loại có thể thay đổi chỉ khi thực sự có lý do để làm như vậy ...

Và để trả lời câu hỏi " Tại sao các loại bất biến hiếm khi được sử dụng trong các ứng dụng khác? " - Tôi thực sự không nghĩ rằng chúng ở bất cứ nơi nào bạn nhìn mọi người khuyên bạn nên biến các lớp của mình thành bất biến như chúng có thể ... ví dụ: http://www.javapractices.com/topic/TopicAction.do?Id=29


1
Cả hai vấn đề của bạn không có ở Haskell.
Florian Margaine

@FlorianMargaine Bạn có thể giải thích?
mrpyo

Sự chậm chạp là không đúng nhờ một trình biên dịch thông minh. Và trong Haskell, ngay cả I / O cũng thông qua API bất biến.
Florian Margaine

2
Một vấn đề cơ bản hơn tốc độ là rất khó để các đối tượng bất biến duy trì danh tính trong khi trạng thái của chúng thay đổi. Nếu một Carđối tượng có thể thay đổi được cập nhật liên tục với vị trí của một ô tô vật lý cụ thể, thì nếu tôi có tham chiếu đến đối tượng đó, tôi có thể tìm ra nơi ở của ô tô đó một cách nhanh chóng và dễ dàng. Nếu Carkhông thay đổi, việc tìm kiếm nơi ở hiện tại có thể sẽ khó hơn nhiều.
supercat

Đôi khi bạn phải mã hóa một cách khá thông minh để trình biên dịch nhận ra rằng không có tham chiếu nào đến đối tượng trước đó bị bỏ lại và do đó có thể sửa đổi nó tại chỗ hoặc thực hiện các phép biến đổi phá rừng, et al. Đặc biệt là trong các chương trình lớn hơn. Và như @supercat nói, danh tính thực sự có thể trở thành một vấn đề.
Macke

0

Để mô hình hóa bất kỳ hệ thống trong thế giới thực nào mà mọi thứ có thể thay đổi, trạng thái có thể thay đổi sẽ cần được mã hóa ở đâu đó, bằng cách nào đó. Có ba cách chính mà một đối tượng có thể giữ trạng thái có thể thay đổi:

  • Sử dụng một tham chiếu có thể thay đổi đến một đối tượng bất biến
  • Sử dụng một tham chiếu bất biến cho một đối tượng có thể thay đổi
  • Sử dụng một tham chiếu có thể thay đổi đến một đối tượng có thể thay đổi

Sử dụng đầu tiên giúp đối tượng dễ dàng thực hiện một ảnh chụp nhanh bất biến của trạng thái hiện tại. Sử dụng cái thứ hai giúp đối tượng dễ dàng tạo ra một cái nhìn trực tiếp về trạng thái hiện tại. Sử dụng thứ ba đôi khi có thể làm cho một số hành động nhất định hiệu quả hơn trong trường hợp ít có nhu cầu mong đợi cho ảnh chụp nhanh bất biến cũng như chế độ xem trực tiếp.

Ngoài thực tế là việc cập nhật trạng thái được lưu trữ bằng cách sử dụng tham chiếu có thể thay đổi thành đối tượng không thay đổi thường chậm hơn trạng thái cập nhật được lưu trữ bằng đối tượng có thể thay đổi, sử dụng tham chiếu có thể thay đổi sẽ yêu cầu người ta từ bỏ khả năng xây dựng chế độ xem trực tiếp giá rẻ của trạng thái. Nếu một người không cần tạo chế độ xem trực tiếp, đó không phải là vấn đề; tuy nhiên, nếu người ta cần tạo chế độ xem trực tiếp, việc không thể sử dụng tham chiếu không thay đổi sẽ thực hiện tất cả các hoạt động với chế độ xem - cả đọc và ghi--much chậm hơn so với họ sẽ được. Nếu nhu cầu về ảnh chụp nhanh bất biến vượt quá nhu cầu về chế độ xem trực tiếp, thì hiệu suất của ảnh chụp nhanh không thay đổi có thể biện minh cho lượt truy cập hiệu suất cho chế độ xem trực tiếp, nhưng nếu một người cần xem trực tiếp và không cần ảnh chụp nhanh, thì sử dụng tham chiếu bất biến cho các đối tượng có thể thay đổi là cách đi.


0

Trong trường hợp của bạn, câu trả lời chủ yếu là do C # hỗ trợ kém cho tính bất biến ...

Sẽ thật tuyệt nếu:

  • mọi thứ sẽ được thay đổi theo mặc định trừ khi có ghi chú khác (nghĩa là với từ khóa 'có thể thay đổi'), việc trộn các loại không thay đổi và có thể thay đổi là khó hiểu

  • phương thức đột biến ( With) sẽ tự động khả dụng - mặc dù điều này đã có thể đạt được, xem với

  • sẽ có một cách để nói kết quả của một cuộc gọi phương thức cụ thể (nghĩa là ImmutableList<T>.Add) không thể bị loại bỏ hoặc ít nhất sẽ tạo ra một cảnh báo

  • Và chủ yếu là nếu trình biên dịch có thể đảm bảo tính bất biến ở nơi được yêu cầu (xem https://github.com/dotnet/roslyn/issues/159 )


1
Điểm thứ ba, ReSharper có một MustUseReturnValueAttributethuộc tính tùy chỉnh thực hiện chính xác điều đó. PureAttributecó tác dụng tương tự và thậm chí còn tốt hơn cho việc này.
Sebastian Redl

-1

Tại sao các loại bất biến rất hiếm khi được sử dụng trong các ứng dụng khác?

Vô minh? Thiếu kinh nghiệm?

Các đối tượng bất biến được coi là ưu việt ngày nay, nhưng đó là một sự phát triển tương đối gần đây. Các kỹ sư không cập nhật hoặc đơn giản là bị mắc kẹt trong 'những gì họ biết' sẽ không sử dụng chúng. Và phải mất một chút thay đổi thiết kế để sử dụng chúng một cách hiệu quả. Nếu các ứng dụng đã cũ hoặc các kỹ sư yếu về kỹ năng thiết kế thì việc sử dụng chúng có thể gây khó xử hoặc gây rắc rối.


"Các kỹ sư đã không cập nhật": Người ta có thể nói rằng một kỹ sư cũng nên tìm hiểu về các công nghệ phi chính thống. Ý tưởng về tính bất biến chỉ mới trở thành xu hướng gần đây, nhưng nó là một ý tưởng khá cũ và được hỗ trợ (nếu không được thi hành) bởi các ngôn ngữ cũ như Scheme, SML, Haskell. Vì vậy, bất cứ ai đã quen nhìn xa hơn các ngôn ngữ chính có thể đã học về nó thậm chí 30 năm trước.
Giorgio

@Giorgio: ở một số quốc gia, nhiều kỹ sư vẫn viết mã C # không có LINQ, không có FP, không có trình lặp và không có chung chung, vì vậy, bằng cách nào đó, họ đã bỏ lỡ mọi thứ xảy ra với C # kể từ năm 2003. Nếu họ thậm chí không biết ngôn ngữ của họ , Tôi hầu như không tưởng tượng họ biết bất kỳ ngôn ngữ phi chính thống nào.
Arseni Mourzenko

@MainMa: Thật tốt khi bạn đã viết các kỹ sư từ in nghiêng.
Giorgio

@Giorgio: ở nước tôi, họ cũng được gọi là kiến trúc sư , chuyên gia tư vấn và rất nhiều thuật ngữ táo bạo khác, không bao giờ được viết bằng chữ in nghiêng. Trong công ty tôi hiện đang làm việc, tôi được gọi là nhà phát triển phân tích và tôi dự kiến ​​sẽ dành thời gian để viết CSS cho mã HTML kế thừa tào lao. Chức danh công việc đang làm phiền trên nhiều cấp độ.
Arseni Mourzenko

1
@MainMa: Tôi đồng ý. Các tiêu đề như kỹ sư hoặc nhà tư vấn thường chỉ là từ thông dụng không có ý nghĩa được chấp nhận rộng rãi. Chúng thường được sử dụng để làm cho ai đó hoặc vị trí của họ quan trọng / uy tín hơn thực tế.
Giorgio
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.