Java tương đương với các phương thức mở rộng C #


176

Tôi đang tìm cách thực hiện một chức năng trong một danh sách các đối tượng như trong C # bằng cách sử dụng một phương thức mở rộng.

Một cái gì đó như thế này:

List<DataObject> list;
// ... List initialization.
list.getData(id);

Làm thế nào để tôi làm điều đó trong Java?


8
Kiểm tra cái này: github.com/nicholas22/jpropel, ví dụ: new String [] {"james", "john", "john", "eddie"} .where (startedWith ("j")). Differ (); Nó sử dụng lombok-pg cung cấp độ tốt của phương thức mở rộng.
NT_

6
Microsoft chắc chắn đã làm đúng khi họ cho phép mở rộng. Phân lớp để thêm chức năng mới không hoạt động nếu tôi cần chức năng trong một lớp trả lại cho tôi ở nơi khác. Giống như thêm các phương thức vào Chuỗi và Ngày.
tggagne

3
tức là java.lang.String là lớp cuối cùng, vì vậy bạn không thể mở rộng nó. Sử dụng các phương thức tĩnh là một cách nhưng đôi khi nó hiển thị mã không thể đọc được. Tôi nghĩ C # để lại một tuổi như một lang máy tính. Các phương thức mở rộng, các lớp một phần, LINQ, v.v.
Davut Gürbüz

7
@Roadrunner, rofl! Câu trả lời tốt nhất cho một tính năng ngôn ngữ bị thiếu là nói rằng tính năng ngôn ngữ bị thiếu là xấu xa và không mong muốn. Điều này được biết.
Kirk Woll

6
Phương pháp mở rộng không phải là "xấu xa". Họ cải thiện đáng kể khả năng đọc mã. Chỉ là một trong nhiều quyết định thiết kế nghèo nàn trong Java.
csauve 29/07/2015

Câu trả lời:


196

Java không hỗ trợ các phương thức mở rộng.

Thay vào đó, bạn có thể tạo một phương thức tĩnh thông thường hoặc viết lớp của riêng bạn.


63
Tôi hư hỏng sau khi sử dụng các phương thức mở rộng - nhưng các phương thức tĩnh cũng sẽ thực hiện thủ thuật này.
bbqchickenrobot

31
Nhưng cú pháp rất hay và giúp chương trình dễ hiểu hơn :) Tôi cũng thích cách Ruby cho phép bạn thực hiện gần như điều tương tự, ngoại trừ bạn thực sự có thể sửa đổi các lớp được xây dựng và thêm các phương thức mới.
biết đến

18
@Ken: Vâng, và đó là toàn bộ vấn đề! Tại sao bạn viết bằng Java mà không trực tiếp bằng mã byte JVM? Không phải "chỉ là vấn đề cú pháp" sao?
Fyodor Soikin

30
Các phương thức mở rộng có thể làm cho mã thanh lịch hơn nhiều so với các phương thức tĩnh thêm từ một số lớp khác. Hầu như tất cả các ngôn ngữ hiện đại hơn cho phép một số loại mở rộng lớp hiện có: C #, php, object-c, javascript. Java chắc chắn cho thấy tuổi của nó ở đây. Hãy tưởng tượng bạn muốn viết JSONObject vào đĩa. Bạn có gọi jsonobj.writeToDisk () hoặc someunrelated class.writeToDisk (jsonobj) không?
woens

8
Những lý do để ghét Java tiếp tục phát triển. Và tôi đã ngừng tìm kiếm họ vài năm trước ......
John Demetriou

54

Các phương thức mở rộng không chỉ là phương thức tĩnh và không chỉ là cú pháp tiện lợi, trên thực tế chúng là công cụ khá mạnh mẽ. Điều chính là khả năng ghi đè các phương thức khác nhau dựa trên khởi tạo tham số chung khác nhau. Điều này tương tự như các lớp loại của Haskell và trên thực tế, có vẻ như chúng ở trong C # để hỗ trợ Monads của C # (tức là LINQ). Ngay cả khi bỏ cú pháp LINQ, tôi vẫn không biết cách nào để thực hiện các giao diện tương tự trong Java.

Và tôi không nghĩ rằng có thể triển khai chúng trong Java, bởi vì ngữ nghĩa xóa kiểu của các tham số chung.


Chúng cũng cho phép bạn thừa hưởng nhiều hành vi (sans đa hình). Bạn có thể thực hiện nhiều giao diện và đi kèm với đó là các phương thức mở rộng của chúng. Chúng cũng cho phép bạn thực hiện hành vi bạn muốn đính kèm vào một loại mà không cần liên kết toàn cầu với loại toàn hệ thống.
Thần kinh học thông minh

25
Toàn bộ câu trả lời này là sai. Các phương thức mở rộng trong C # chỉ là đường cú pháp mà trình biên dịch sắp xếp lại một chút để di chuyển mục tiêu của lệnh gọi phương thức sang đối số đầu tiên của phương thức tĩnh. Bạn không thể ghi đè các phương thức hiện có. Không có yêu cầu rằng một phương pháp mở rộng là một đơn nguyên. Nghĩa đen của nó chỉ là một cách thuận tiện hơn để gọi một phương thức tĩnh, cho phép xuất hiện thêm các phương thức cá thể vào một lớp. Vui lòng đọc phần này nếu bạn đồng ý với câu trả lời này
Matt Klein

3
tốt, trong trường hợp này, xác định đường cú pháp là gì, tôi sẽ gọi một đường cú pháp là cú pháp macro bên trong, đối với trình biên dịch phương thức mở rộng ít nhất phải tra cứu lớp tĩnh mà phương thức mở rộng được đặt để thay thế. Không có gì trong câu trả lời về phương pháp nên đơn điệu là vô nghĩa. Ngoài ra, bạn có thể sử dụng nó để nạp chồng, nhưng nó không phải là tính năng của các phương thức mở rộng, nó là quá tải dựa trên kiểu tham số đơn giản, giống như cách nó sẽ hoạt động nếu phương thức được gọi trực tiếp và nó sẽ không hoạt động trong nhiều trường hợp thú vị trong Java bởi vì khái quát loại lập luận xóa.
dùng1686250

1
@ user1686250 Có thể triển khai bằng Java (bằng "Java" Tôi giả sử bạn có nghĩa là mã byte chạy trên JVM) ... Kotlin, biên dịch thành mã byte có các phần mở rộng. Nó chỉ là cú pháp đường trên các phương pháp tĩnh. Bạn có thể sử dụng trình dịch ngược trong IntelliJ để xem Java tương đương trông như thế nào.
Jeffrey Blattman

@ user1686250 Bạn có thể phát triển những gì bạn đang viết về thuốc generic (hoặc đưa ra một liên kết) bởi vì tôi hoàn toàn không hiểu ý về thuốc generic. Theo cách nào khác với các phương thức tĩnh thông thường thì nó có liên quan đến?
C.Champagne


10

Về mặt kỹ thuật C # Extension không có tương đương trong Java. Nhưng nếu bạn muốn thực hiện các chức năng như vậy để có mã sạch hơn và khả năng bảo trì, bạn phải sử dụng khung Manifold.

package extensions.java.lang.String;

import manifold.ext.api.*;

@Extension
public class MyStringExtension {

  public static void print(@This String thiz) {
    System.out.println(thiz);
  }

  @Extension
  public static String lineSeparator() {
    return System.lineSeparator();
  }
}

7

Các Xtend ngôn ngữ - đó là một siêu bộ Java, và biên dịch Java mã nguồn 1  - hỗ trợ này.


Khi mã không phải là Java được biên dịch sang Java, bạn có phương thức mở rộng không? Hoặc mã Java chỉ là một phương thức tĩnh?
Fabio Milheiro

@Bomboca Như những người khác đã lưu ý, Java không có các phương thức mở rộng. Vì vậy, mã XTend, được biên dịch sang Java, bằng cách nào đó không tạo ra một phương thức mở rộng Java. Nhưng nếu bạn làm việc riêng ở XTend, bạn sẽ không chú ý hay quan tâm. Nhưng, để trả lời câu hỏi của bạn, bạn cũng không nhất thiết phải có một phương pháp tĩnh. Tác giả chính của XTend có một mục blog về điều này tại blog.efftinge.de/2011/11/iêu
Erick G. Hagstrom

Vâng, không biết tại sao tôi cũng không nghĩ như vậy. Cảm ơn!
Fabio Milheiro

@Sam Cảm ơn bạn đã giới thiệu tôi với XTend - Tôi chưa bao giờ nghe về nó.
jpaugh

7

Manifold cung cấp cho Java các phương thức mở rộng kiểu C # và một số tính năng khác. Không giống như các công cụ khác, Manifold không có giới hạn và không gặp vấn đề với generic, lambdas, IDE, v.v. Manifold cung cấp một số tính năng khác như các loại tùy chỉnh kiểu F # , giao diện cấu trúc kiểu TypeScript và các kiểu mở rộng kiểu Javascript .

Ngoài ra, IntelliJ cung cấp hỗ trợ toàn diện cho Manifold thông qua plugin Manifold .

Manifold là một dự án nguồn mở có sẵn trên github .



5

Java không có tính năng như vậy. Thay vào đó, bạn có thể tạo lớp con thông thường của việc thực hiện danh sách của mình hoặc tạo lớp bên trong ẩn danh:

List<String> list = new ArrayList<String>() {
   public String getData() {
       return ""; // add your implementation here. 
   }
};

Vấn đề là gọi phương thức này. Bạn có thể làm điều đó "tại chỗ":

new ArrayList<String>() {
   public String getData() {
       return ""; // add your implementation here. 
   }
}.getData();

112
Điều đó là hoàn toàn vô dụng.
SLaks

2
@Slaks: Tại sao chính xác? Đây là một "lớp học của riêng bạn" được đề xuất bởi chính bạn.
Goran Jovic

23
@Goran: Tất cả điều này cho phép bạn làm là xác định một phương thức, sau đó gọi nó ngay lập tức, một lần .
SL

3
@Slaks: Được rồi, lấy điểm. So với giải pháp hạn chế đó, viết một lớp có tên sẽ tốt hơn.
Goran Jovic

Có một sự khác biệt lớn, lớn giữa các phương thức mở rộng C # và các lớp ẩn danh Java. Trong C #, một phương thức mở rộng là đường cú pháp cho những gì thực sự chỉ là một phương thức tĩnh. IDE và trình biên dịch làm cho một phương thức mở rộng xuất hiện như thể nó là một phương thức thể hiện của lớp mở rộng. (Lưu ý: "mở rộng" trong ngữ cảnh này không có nghĩa là "được kế thừa" như thường thấy trong Java.)
HairOfTheDog

4

Dường như có một số cơ hội nhỏ mà các Phương thức bảo vệ (tức là các phương thức mặc định) có thể biến nó thành Java 8. Tuy nhiên, theo tôi hiểu, chúng chỉ cho phép tác giả của một bản interfacemở rộng hồi tố, chứ không phải người dùng tùy ý.

Phương thức bảo vệ + Tiêm giao diện sau đó có thể thực hiện đầy đủ các phương thức mở rộng kiểu C #, nhưng AFAICS, Giao diện tiêm thậm chí chưa có trên bản đồ đường 8 của Java.


3

Hơi muộn với bữa tiệc về câu hỏi này, nhưng trong trường hợp bất cứ ai thấy nó hữu ích, tôi chỉ tạo một lớp con:

public class ArrayList2<T> extends ArrayList<T> 
{
    private static final long serialVersionUID = 1L;

    public T getLast()
    {
        if (this.isEmpty())
        {
            return null;
        }
        else
        {       
            return this.get(this.size() - 1);
        }
    }
}

4
Các phương thức mở rộng thường dành cho mã không thể sửa đổi hoặc kế thừa như các lớp cuối cùng / niêm phong và sức mạnh chính của nó là phần mở rộng của Giao diện, ví dụ như mở rộng IEnumerable <T>. Tất nhiên, chúng chỉ là đường cú pháp cho các phương pháp tĩnh. Mục đích là, mã dễ đọc hơn nhiều. Mã sạch hơn có nghĩa là bảo trì / khả năng tiến hóa tốt hơn.
mbx

1
Đó không chỉ là nó @mbx. Các phương thức mở rộng cũng hữu ích để mở rộng chức năng lớp của các lớp không được niêm phong nhưng bạn không thể mở rộng vì bạn không kiểm soát bất kỳ trường hợp nào trả về, ví dụ: HTTPContextBase là một lớp trừu tượng.
Fabio Milheiro

@FabioMilheiro Tôi hào phóng bao gồm các lớp trừu tượng là "giao diện" trong bối cảnh đó. Các lớp được tạo tự động (xsd.exe) là cùng loại: bạn có thể nhưng không nên mở rộng chúng bằng cách sửa đổi các tệp được tạo. Bạn thường sẽ mở rộng chúng bằng cách sử dụng "một phần" yêu cầu chúng nằm trong cùng một cụm. Nếu không, các phương thức mở rộng là một lựa chọn khá dễ nhìn. Cuối cùng, chúng chỉ là các phương thức tĩnh (không có sự khác biệt nếu bạn nhìn vào Mã IL được tạo).
mbx

Có ... HttpContextBase là một sự trừu tượng mặc dù tôi hiểu sự hào phóng của bạn. Gọi một giao diện là một sự trừu tượng có vẻ có vẻ tự nhiên hơn. Bất kể điều đó, tôi không có nghĩa là nó phải là một sự trừu tượng. Tôi chỉ đưa ra một ví dụ về một lớp mà tôi đã viết nhiều phương thức mở rộng.
Fabio Milheiro

2

Chúng ta có thể mô phỏng việc triển khai các phương thức mở rộng C # trong Java bằng cách sử dụng triển khai phương thức mặc định có sẵn từ Java 8. Chúng ta bắt đầu bằng cách xác định một giao diện cho phép chúng ta truy cập đối tượng hỗ trợ thông qua phương thức cơ sở (), như vậy:

public interface Extension<T> {

    default T base() {
        return null;
    }
}

Chúng tôi trả về null vì các giao diện không thể có trạng thái, nhưng điều này phải được sửa sau đó thông qua proxy.

Nhà phát triển tiện ích mở rộng sẽ phải mở rộng giao diện này bằng giao diện mới chứa các phương thức mở rộng. Giả sử chúng tôi muốn thêm một người tiêu dùng forEach trên giao diện Danh sách:

public interface ListExtension<T> extends Extension<List<T>> {

    default void foreach(Consumer<T> consumer) {
        for (T item : base()) {
            consumer.accept(item);
        }
    }

}

Vì chúng tôi mở rộng giao diện Tiện ích mở rộng, chúng tôi có thể gọi phương thức cơ sở () bên trong phương thức tiện ích mở rộng của mình để truy cập đối tượng hỗ trợ mà chúng tôi đính kèm.

Giao diện Tiện ích mở rộng phải có phương thức xuất xưởng sẽ tạo ra phần mở rộng của đối tượng hỗ trợ nhất định:

public interface Extension<T> {

    ...

    static <E extends Extension<T>, T> E create(Class<E> type, T instance) {
        if (type.isInterface()) {
            ExtensionHandler<T> handler = new ExtensionHandler<T>(instance);
            List<Class<?>> interfaces = new ArrayList<Class<?>>();
            interfaces.add(type);
            Class<?> baseType = type.getSuperclass();
            while (baseType != null && baseType.isInterface()) {
                interfaces.add(baseType);
                baseType = baseType.getSuperclass();
            }
            Object proxy = Proxy.newProxyInstance(
                    Extension.class.getClassLoader(),
                    interfaces.toArray(new Class<?>[interfaces.size()]),
                    handler);
            return type.cast(proxy);
        } else {
            return null;
        }
    }
}

Chúng tôi tạo một proxy thực hiện giao diện mở rộng và tất cả giao diện được triển khai theo loại đối tượng hỗ trợ. Trình xử lý lời gọi được cung cấp cho proxy sẽ gửi tất cả các cuộc gọi đến đối tượng hỗ trợ, ngoại trừ phương thức "cơ sở", phải trả về đối tượng hỗ trợ, nếu không, việc triển khai mặc định của nó sẽ trả về null:

public class ExtensionHandler<T> implements InvocationHandler {

    private T instance;

    private ExtensionHandler(T instance) {
        this.instance = instance;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        if ("base".equals(method.getName())
                && method.getParameterCount() == 0) {
            return instance;
        } else {
            Class<?> type = method.getDeclaringClass();
            MethodHandles.Lookup lookup = MethodHandles.lookup()
                .in(type);
            Field allowedModesField = lookup.getClass().getDeclaredField("allowedModes");
            makeFieldModifiable(allowedModesField);
            allowedModesField.set(lookup, -1);
            return lookup
                .unreflectSpecial(method, type)
                .bindTo(proxy)
                .invokeWithArguments(args);
        }
    }

    private static void makeFieldModifiable(Field field) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField
                .setInt(field, field.getModifiers() & ~Modifier.FINAL);
    }

}

Sau đó, chúng ta có thể sử dụng phương thức Extension.create () để đính kèm giao diện chứa phương thức mở rộng vào đối tượng hỗ trợ. Kết quả là một đối tượng có thể được truyền tới giao diện mở rộng mà chúng ta vẫn có thể truy cập đối tượng hỗ trợ gọi phương thức base (). Có tham chiếu được truyền tới giao diện mở rộng, bây giờ chúng ta có thể gọi một cách an toàn các phương thức mở rộng có thể có quyền truy cập vào đối tượng hỗ trợ, để bây giờ chúng ta có thể đính kèm các phương thức mới vào đối tượng hiện có, nhưng không phải là kiểu xác định của nó:

public class Program {

    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c");
        ListExtension<String> listExtension = Extension.create(ListExtension.class, list);
        listExtension.foreach(System.out::println);
    }

}

Vì vậy, đây là cách chúng ta có thể mô phỏng khả năng mở rộng các đối tượng trong Java bằng cách thêm các hợp đồng mới vào chúng, cho phép chúng ta gọi các phương thức bổ sung trên các đối tượng đã cho.

Dưới đây bạn có thể tìm thấy mã của giao diện Tiện ích mở rộng:

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

public interface Extension<T> {

    public class ExtensionHandler<T> implements InvocationHandler {

        private T instance;

        private ExtensionHandler(T instance) {
            this.instance = instance;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            if ("base".equals(method.getName())
                    && method.getParameterCount() == 0) {
                return instance;
            } else {
                Class<?> type = method.getDeclaringClass();
                MethodHandles.Lookup lookup = MethodHandles.lookup()
                    .in(type);
                Field allowedModesField = lookup.getClass().getDeclaredField("allowedModes");
                makeFieldModifiable(allowedModesField);
                allowedModesField.set(lookup, -1);
                return lookup
                    .unreflectSpecial(method, type)
                    .bindTo(proxy)
                    .invokeWithArguments(args);
            }
        }

        private static void makeFieldModifiable(Field field) throws Exception {
            field.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        }

    }

    default T base() {
        return null;
    }

    static <E extends Extension<T>, T> E create(Class<E> type, T instance) {
        if (type.isInterface()) {
            ExtensionHandler<T> handler = new ExtensionHandler<T>(instance);
            List<Class<?>> interfaces = new ArrayList<Class<?>>();
            interfaces.add(type);
            Class<?> baseType = type.getSuperclass();
            while (baseType != null && baseType.isInterface()) {
                interfaces.add(baseType);
                baseType = baseType.getSuperclass();
            }
            Object proxy = Proxy.newProxyInstance(
                    Extension.class.getClassLoader(),
                    interfaces.toArray(new Class<?>[interfaces.size()]),
                    handler);
            return type.cast(proxy);
        } else {
            return null;
        }
    }

}

1
đó là một địa ngục của bùn!
Dmitry Avtonomov

1

Người ta có thể sử dụng mẫu thiết kế hướng đối tượng trang trí . Một ví dụ về mẫu này đang được sử dụng trong thư viện chuẩn của Java sẽ là DataOutputStream .

Đây là một số mã để tăng cường chức năng của Danh sách:

public class ListDecorator<E> implements List<E>
{
    public final List<E> wrapee;

    public ListDecorator(List<E> wrapee)
    {
        this.wrapee = wrapee;
    }

    // implementation of all the list's methods here...

    public <R> ListDecorator<R> map(Transform<E,R> transformer)
    {
        ArrayList<R> result = new ArrayList<R>(size());
        for (E element : this)
        {
            R transformed = transformer.transform(element);
            result.add(transformed);
        }
        return new ListDecorator<R>(result);
    }
}

PS Tôi là một fan hâm mộ lớn của Kotlin . Nó có các phương thức mở rộng và cũng chạy trên JVM.


0

Bạn có thể tạo phương thức tiện ích mở rộng / trợ giúp C # bằng cách (RE) triển khai giao diện Bộ sưu tập và thêm ví dụ cho Bộ sưu tập Java:

public class RockCollection<T extends Comparable<T>> implements Collection<T> {
private Collection<T> _list = new ArrayList<T>();

//###########Custom extension methods###########

public T doSomething() {
    //do some stuff
    return _list  
}

//proper examples
public T find(Predicate<T> predicate) {
    return _list.stream()
            .filter(predicate)
            .findFirst()
            .get();
}

public List<T> findAll(Predicate<T> predicate) {
    return _list.stream()
            .filter(predicate)
            .collect(Collectors.<T>toList());
}

public String join(String joiner) {
    StringBuilder aggregate = new StringBuilder("");
    _list.forEach( item ->
        aggregate.append(item.toString() + joiner)
    );
    return aggregate.toString().substring(0, aggregate.length() - 1);
}

public List<T> reverse() {
    List<T> listToReverse = (List<T>)_list;
    Collections.reverse(listToReverse);
    return listToReverse;
}

public List<T> sort(Comparator<T> sortComparer) {
    List<T> listToReverse = (List<T>)_list;
    Collections.sort(listToReverse, sortComparer);
    return listToReverse;
}

public int sum() {
    List<T> list = (List<T>)_list;
    int total = 0;
    for (T aList : list) {
        total += Integer.parseInt(aList.toString());
    }
    return total;
}

public List<T> minus(RockCollection<T> listToMinus) {
    List<T> list = (List<T>)_list;
    int total = 0;
    listToMinus.forEach(list::remove);
    return list;
}

public Double average() {
    List<T> list = (List<T>)_list;
    Double total = 0.0;
    for (T aList : list) {
        total += Double.parseDouble(aList.toString());
    }
    return total / list.size();
}

public T first() {
    return _list.stream().findFirst().get();
            //.collect(Collectors.<T>toList());
}
public T last() {
    List<T> list = (List<T>)_list;
    return list.get(_list.size() - 1);
}
//##############################################
//Re-implement existing methods
@Override
public int size() {
    return _list.size();
}

@Override
public boolean isEmpty() {
    return _list == null || _list.size() == 0;
}

-7

Java8 hiện hỗ trợ các phương thức mặc định , tương tự như C#các phương thức mở rộng.


9
Sai lầm; ví dụ trong câu hỏi này vẫn không thể.
SLaks

@SLaks sự khác biệt giữa các phần mở rộng Java và C # là gì?
Fabio Milheiro

3
Các phương thức mặc định chỉ có thể được xác định trong giao diện. docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
SLaks

DarVar, Chủ đề bình luận này là nơi duy nhất mà các phương thức mặc định được đề cập, mà tôi đang cố gắng ghi nhớ một cách tuyệt vọng. Cảm ơn đã đề cập đến họ, nếu không bằng tên! :-) (Cảm ơn @SLaks cho liên kết)
jpaugh

Tôi sẽ đưa ra câu trả lời đó, vì kết quả cuối cùng từ một phương thức tĩnh trong giao diện sẽ cung cấp cách sử dụng giống như các phương thức mở rộng C #, tuy nhiên, bạn vẫn cần triển khai giao diện trong lớp của mình không giống như từ khóa C # bạn truyền (từ khóa này) như một tham số
zaPlayer
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.