Follow us on...
Follow us on Facebook

VN-Zoom.com chung tay vì Cộng đồng

Kaka - ứng dụng hát Karaoke trên mobile

Tuyển Mod Mobile diễn dàn Vn-Zoom.com 2014

Chiêm ngưỡng BaoMoi đẹp "tuyệt diệu" trên Windows Phone

Vui thể thao quà ý nghĩa

Toàn cảnh Vn-Zoom tham gia họp báo Asus Zenfone
kết quả từ 1 tới 4 trên 4
  1. #1
    phong_robin's Avatar
    phong_robin vẫn chưa có mặt trong diễn đàn Rìu Bạc Đôi
    Tham gia
    Dec 2007
    Bài
    497
    Cảm ơn
    35
    Điểm
    742/209 bài viết
    VR power
    0

    Exclamation So sánh cú pháp của C# và Java



    Trong loạt article đầu tiên, chúng tôi sẽ chuyển một chương trình nhỏ từ Java sang C# và chỉ ra sự khác nhau giữa hai ngôn ngữ này. Xuyên suốt quá trình này, chúng ta sẽ tập trung vào những đặc điểm của ngôn ngữ C#. Những gì tôi sẽ làm là chỉ ra các cấu trúc cú pháp giống nhau ở điểm nào và dành thời gian để nói về sự khác nhau giữa chúng. Có một vài cú pháp “mở rộng” trong C# không giống như trong Java, và tôi sẽ dành thời gian để chỉ ra những điều này khi chúng ta đi tiếp. Hãy bắt đầu từ những điều nhỏ và đi tới những điều lớn hơn.

    1.Các kiểu nguyên gốc (primitive) và kiểu đơn giản (simple)

    Java có một vài kiểu primitive mà mọi người rất thân thuộc: byte, char, int, long, float, double. Những kiểu primitive là những khối được xây dựng cơ bản của Java, chúng là những “đơn vị” nhỏ nhất. Những gì thường gây khó chịu đối với hầu hết lập trình viên là kiểu primitive thường tách rời khỏi mô hình đối tượng của Java; trong khi tất cả các đối tượng trong Java đều kế thừa từ java.lang.Object, các kiểu primitive không kế thừa từ bất kỳ gì cả. Điều này có nghĩa là bất kỳ một lớp nào khi tính toán trên các đối tượng (ví dụ như các đối tượng trong Tập hợp API) sẽ không làm việc với các kiểu primitive. Các kiểu primitive sẽ phải được ánh xạ (map) thành mô hình đối tượng theo quy định để có thể sử dụng chúng.
    Không có những trường hợp như thế trong C#. C# sử dụng hệ thống kiểu/đối tượng trong .NET mà ở đó, các chương trình C# có thể giao tiếp với nhiều ngôn ngữ khác trong .NET mà không gặp rắc rối nào về kiểu. Ví dụ, kiểu int là một bí danh của System.Int32 được kế thừa cuối cùng từ System.Object. Điều này có nghĩa là các kiểu primitive, hay kiểu simple trong hàm C# cũng giống như bất kỳ các đối tượng khác. Ví dụ, điều này là đúng khi gọi phương thức toString hoặc GetType trong bất kỳ một kiểu primitive nào.



    Mặc dù các kiểu simple trong C# là những đối tượng, tuy nhiên chúng vẫn được truyền theo tham trị (pass-by-value) tương tự như trong Java. Đây là trường hợp khác, bởi vì ngoài việc là những đối tượng, tất cả các kiểu simple trong C# đều là các đối tượng – cấu trúc (struct) khi được truyền theo tham trị sẽ được truyền theo tham biến một lần nữa (chúng ta sẽ đề cập đến phần này sau).

    2.Các phát biểu (statements)

    Bây giờ chúng ta có những cấu trúc dữ liệu primitive, chúng ta nên tổ chức chúng ở những phát biểu ở cấp độ cao hơn. Bạn sẽ để ý rằng, không cần phải nói nhiều với các lập trình viên Java bởi họ đã rất thân thuộc với các cú pháp này, chúng ta sẽ chỉ tập trung vào sự khác nhau giữa C# và Java

    3.Khai báo (declarations)

    Các biến được định nghĩa trong C# cũng giống như trong Java

    int integer = 3;
    bool boolean = true;

    Java sử dụng các từ khóa “static final” để tạo nên các biến hằng; trong Java một biến “static final” là một biến lớp thay vì là một biến đối tượng, và trình biên dịch sẽ ngăn ngừa bất kỳ các đối tượng khác thay đổi giá trị của biến. C#, theo quy định, có hai cách công bố một biến hằng.
    Đánh dấu một biến bằng từ khóa const sẽ làm cho giá trị được chuyển đổi trước khi biên dịch. Với định nghĩa sau:
    const int two = 2;
    phát biểu
    2 * two
    được chuyển thành
    2 * 2
    bằng vi xử lý trước khi biên dịch. Điều này sẽ làm cho chương trình đã được biên dịch sẽ chạy nhanh hơn bởi nó không phải tìm kiếm giá trị của hằng trong suốt thời gian chạy (run-time).
    Xem rằng các hằng thường được sử dụng cho BUFFERSIZE hoặc TIMEOUT, điều này có thể sẽ không gây ra một sự chuyển đổi bên trong đoạn mã; nếu một field được đánh dấu là const, khi đó bất kỳ một đoạn mã nào biên dịch nó một lần nữa sẽ không chuyển đổi hằng và sẽ cần được biên dịch lại theo quy định để thay đổi nó. Thay vì thế, nếu một hằng được đánh dấu là readonly, khi đó ứng dụng được thực thi vào lần tới, trạng thái sẽ thay đổi cũng như đoạn mã sẽ được kiểm tra giá trị của field readonly, trong khi trình biên dịch vẫn bảo vệ nó không thay đổi theo chương trình.

    (Theo: OnDotNet)

  2. #2
    phong_robin's Avatar
    phong_robin vẫn chưa có mặt trong diễn đàn Rìu Bạc Đôi
    Tham gia
    Dec 2007
    Bài
    497
    Cảm ơn
    35
    Điểm
    742/209 bài viết
    VR power
    0

    Default

    4.Cấu trúc điều kiện (Conditionals structure)

    Có hai cấu trúc điều kiện mà các lập trình viên Java mong đợi, các phát biểu “if–then–else” và “switch”, cả hai đều có sẵn trong C#. Cả hai đều tương tự ngoại trừ một chút khác nhau trong cú pháp phát biểu “switch” của C#.
    Java cho phép dòng điều khiển phải rơi vào chính xác trong các trường hợp khác nhau của phát biểu switch, trong khi trình biên dịch C# tuyệt đối không cho phép điều này – trình biên dịch C# sẽ đánh dấu đây là một lỗi cú pháp. Ví dụ, trong Java những dòng dưới đây là đúng

    int switching = 3;
    switch(switching) {
    case 3:
    System.out.println(“here”);
    default:
    System.out.println(“and here”);
    }


    Kết quả là cả hai dòng lệnh println đều được thực thi. Trong C#, trong định nghĩa dòng điều khiển, nhất thiết phải có một khai báo break hoặc goto case cho những trường hợp khác nhau ở trong mỗi case.

    5.Các vòng lặp (Loops)

    C# cũng có những vòng lặp quen thuộc “while’, “do–while” và “for”. Ngoài ba vòng lặp này, C# còn giới thiệu một vòng lặp “foreach” để tính toán trên các đối tượng nhằm bổ sung giao tiếp System.Collections.Ienumerable. Đặc biệt hơn, các mảng trong C# (System.Array) bổ sung Ienumerable, vì thế đoạn mã Java dưới đây

    int[] array = // …
    for( int count = 0; count < array.length; count ++ ) {
    System.out.println( array[count] );
    }


    là đúng trong C#, nhưng nó cũng có thể được thay thế trong C# như sau:

    foreach( int member in array )
    Console.Writeln( member );


    Đặc biệt, giao tiếp Ienumerable cung cấp khả năng nhận được một sự thay thế Ienumerator cho một đối tượng (giống như java.util.Enumeration). Bất kỳ cái gì bổ sung các giao tiếp Ienumerable và Ienumerator đều có thể được tính toán trên vòng lặp foreach.

    6.Các phát biểu nhảy (Jumps)

    Hầu hết các phát biểu nhảy trong Java đều ánh xạ trong C#: continue, break, goto, return. Các phát biểu này đều sử dụng giống như cách mà chúng được sử dụng trong Java: thoát khỏi các vòng lặp hoặc trả dòng điều khiển cho một khối lệnh khác.
    Những gì quan trọng để thảo luận chính là mô hình ngoại lệ (exception model), cũng như nó khác nhau cả về ngữ nghĩa và cú pháp. Tất cả các ngoại lệ trong C# đều là các ngoại lệ run-time, trình biên dịch sẽ không giúp đỡ các lập trình viên giữ lại trạng thái của các ngoại lệ. Cũng nên chú ý rằng bản thân phương thức không chứa các ngoại lệ được ném ra bên trong các khối lệnh. Cảnh báo của tôi đối với các lập trình viên Java chuyển sang C# là phải rất cẩn thận.
    Khối lệnh “try-catch-finally” mà các lập trình viên Java quen thuộc vẫn tồn tại trong C#. Tất cả các ngoại lệ đều bắt nguồn từ System.Exception, vì thế việc bắt System.Exception sẽ bắt tất cả các ngoại lệ có khả năng được ném ra từ một khối lệnh, cũng giống như trong Java. Có sẵn một cách đi tắt nếu đối tượng ngoại lệ không cần thiết bằng việc sử dụng framework dưới đây

    try {
    // những lệnh có thể gây ra ngoại lệ
    }
    catch {
    // xử lý những ngoại lệ mà không cần nhận một bộ xử lý cho đối tượng ngoại lệ
    }


    Nhưng nếu bạn cần bắt một ngoại lệ cụ thể (trong khi không yêu cầu đối tượng ngoại lệ), có thể dùng tương tự như sau:

    try {
    }
    catch (IOException) {
    }


    7.Các phương thức (methods)

    Ở mức độ cơ bản, không có sự khác nhau giữa các phương thức trong Java và C#, mỗi phương thức đều đặt vào các tham số và có kiểu trả về. Tuy nhiên, C# có những điều chúng ta có thể làm với các phương thức mà chúng ta không thể làm với Java.

    a.params

    Khi gọi một phương thức, trình biên dịch Java sẽ kiểm tra xem có phương thức nào giống với phương thức yêu cầu hay không. Ví dụ trong Java, nếu có một phương thức được định nghĩa

    public void methodCaller ( int a, int b, int c );

    thì khi có một phương thức gọi phương thức đó

    methodCaller( 3, 4, 5, 6 );

    sẽ tạo ra một lỗi biên dịch do trình biên dịch Java không tìm thấy được bất kỳ một phương thức methodCaller() nào đặt vào 4 tham số kiểu int. Chúng ta không thể tạo ra một phương thức yêu cầu rằng cho phép chúng ta đặt vào một số lượng biến kiểu int như là các tham số.
    Tại sao lại không nên có điều này? Nếu một lập trình viên muốn đặt vào một số lượng biến kiểu int, anh ta có thể định nghĩa một phương thức yêu cầu đặt vào một dãy các số nguyên và sau đó khởi dựng dãy này khi gọi phương thức. Trong Java, sẽ có tối thiểu 3 dòng lệnh chỉ để gọi phương thức – dòng đầu tiên tạo ra một dãy, một hoặc nhiều dòng sau gán các giá trị vào cho dãy, dòng thứ ba gọi phương thức. C# thay đổi điều này bằng cách cho phép các lập trình viên đặt vào một tham số trên tham số cuối cùng của dãy của phương thức, do đó một số lượng các tham số có thể đặt vào đó. Sử dụng ví dụ dưới đây, phương thức có thể được định nghĩa như sau:

    public void methodCaller ( params int[] a );

    Và phương thức có thể được gọi bất kỳ

    methodCaller ( 1 );
    methodCaller ( 1, 2, 3, 4 );

    Bên trong phương thức, các tham số có thể được truy cập thông qua dãy “a” đã được định nghĩa.

    b.ref và out

    C# cũng có thể thay đổi một tham số được đặt vào như một tham biến trái với việc đặt vào với một tham trị. Ví dụ với một kiểu primitive

    public void increment( int a ) {
    a ++;
    }

    int a = 3;
    increment ( a );
    // a vẫn bằng 3


    sẽ không làm cho biến được đặt vào phương thức increment() không thật sự được tăng lên bởi vì a được đặt vào như một tham trị, biến a trong khối lệnh của phương thức increment() nằm trong môt môi trường khác và vì thế việc thay đổi trong tầm vực (scope) đó không bị ảnh hưởng đến.
    Tuy nhiên thay đổi phương thức thành

    public void increment ( ref int a ) {
    a ++;

    }

    int a = 3;
    increment ( a );
    // bây giờ thì a đã tăng lên 4


    sẽ làm cho int a được đặt vào như một tham biến (giống như khi đặt vào một lớp) và vì thế việc chỉnh sửa nó sẽ làm cho giá trị gốc bị thay đổi. Sử dụng ref với một lớp cho phép một hàm gán lại con trỏ lớp bên trong hàm và thay đổi được bên ngoài phương thức.
    Một từ khóa đi cặp với ref là out. Trong khi ref không bảo đảm rằng bất kỳ phép toán nào thật sự được tính toán trên biến, sử dụng out sẽ làm cho trình biên dịch chắc chắn rằng một giá trị đã được ấn định cho biến. Ví dụ đoạn mã dưới đây

    public void doNoThing ( out int a ) {
    }


    sẽ làm cho trình biên dịch hiểu rằng a sẽ không được gán cho bất kỳ gì cả.

    (Theo: OnDotNet)

  3. #3
    phong_robin's Avatar
    phong_robin vẫn chưa có mặt trong diễn đàn Rìu Bạc Đôi
    Tham gia
    Dec 2007
    Bài
    497
    Cảm ơn
    35
    Điểm
    742/209 bài viết
    VR power
    0

    Default

    8. Các thuộc tính (properties)

    Các thuộc tính là các khởi dựng của C# thường được dùng với mô hình (pattern) getter/setter trong nhiều lớp của Java. Java có một phương thức set đặt vào một tham số và phương thức get nhận về những gì tham số đã được đặt vào trước đó.

    private int property;
    public int getProperty () {
    return this.property;
    }

    public void setProperty ( int property ) {
    this.property = property;
    }


    Có thể được viết trong C# như sau:

    private int property;
    public int Property () {
    get {
    return this.property;
    }
    set {
    // value là một biến được tạo ra bởi trình biên dịch để thay thế các tham số
    this.property = value;
    }
    }


    Có thể dễ dàng sử dụng bên trong một chương trình C#

    int currentValue = Property;
    Property = new Value;

    Đằng sau ngữ cảnh này, C# thật sự biên dịch property thành hai phương thức trong framework ngôn ngữ trực tiếp .NET (Intermediate Language) có tên là get_Property và set_Property. Các phương thức này không thể gọi trực tiếp từ C#, nhưng những ngôn ngữ khác sử dụng MSIL có thể truy cập các getters/setters này.

    9.Từ chỉ định truy cập (Accessbility Modifiers)

    Access modifier làm những gì mà tên chúng đã thể hiện – chúng giới hạn khả năng thay đổi một vùng của đoạn mã. Các modifier mà chúng ta sử dụng là private, protected, default, public. C# lại có năm modifier:

    * public – cũng giống như trong Java. Bạn có thể nhận được những gì bên trong đối tượng, bất cứ gì đều có thể truy cập tự do đến thành viên này.
    * protected – cũng giống như trong Java. Việc truy cập chỉ dành cho những lớp kế thừa lớp chứa từ khóa này.
    * internal – đây là một từ mới với những lập trình viên Java. Tất cả những đối tượng bạn định nghĩa bên trong một file .cs (bạn có thể định nghĩa nhiều hơn một đối tượng bên trong file .cs, không giống như trong Java bạn thường định nghĩa chỉ một đối tượng) có một bộ xử lý cho các thành viên bên trong.
    * protected internal – từ khóa này xem như là một sự kết hợp giữa protected và internal. Thành phần này có thể được truy cập từ assembly hoặc bên trong những đối tượng kế thừa từ lớp này.
    * private – cũng giống như trong Java. Không có bất kỳ gì có thể truy cập vào lớp ngoại trừ bên trong lớp

    Các modifier này có thể được áp dụng cho cùng các cấu trúc mà Java cho phép bạn sử dụng chúng. Bạn có thể thay đổi khả năng truy cập đến các đối tượng, các phương thức và các biến. Chúng ta sẽ nói về chúng ngay dưới đây, và chúng ta có thể nói về các đối tượng và kế thừa ở phần tiếp theo.

    10.Các đối tượng, các lớp và các cấu trúc

    Tất cả các lập trình viên Java đều đã thân thuộc với các khái niệm về lớp, đối tượng, kế thừa. Vì thế việc học những phần tương tự trong C# chỉ là đề cập đến sự khác nhau của ngữ nghĩa. Định nghĩa một lớp như dưới đây

    class A {
    }

    và kế thừa sẽ sử dụng dấu “:” thay vì sử dụng từ khóa extends

    class B : A {
    }

    nghĩa là lớp B kế thừa từ lớp A.
    Các giao tiếp (interfaces) được bổ sung theo cùng cách như vậy (mặc dù có thể hơi rắc rối với các lập trình viên Java) với giao tiếp C được bổ sung cho lớp D

    class D : C {
    }

    và D có thể kế thừa B và bổ sung C

    class D : B, C {
    }

    Chú ý rằng, B được đặt trước C trong danh sách, và bất kỳ một giao tiếp nào khác được bổ sung sẽ được thêm vào sau danh sách được chia bởi dấu phẩy.
    Tất cả các lớp sẽ được truyền theo tham biến cho các phương thức gọi. Điều này có nghĩa là biến được định nghĩa và được truyền thật sự là một tham biến cho vùng nhớ chứa đối tượng thật sự. Mọi thứ trong Java, ngoại trừ kiểu primitive, đều được truyền theo tham biến – không có cách nào để định nghĩa mọi thứ để có thể truyền theo tham trị. Để định nghĩa một đối tượng nhằm truyền theo tham trị trong C#, cấu trúc mang tên “struct” được sử dụng

    struct E {
    }

    struct trông giống như lớp và cũng hoạt động giống như lớp (tất cả chúng đều được bắt nguồn từ System.Object)

    * struct được truyền theo tham trị thay vì theo tham biến
    * struct không thể kế thừa, tuy nhiên chúng có thể bổ sung các giao tiếp
    * struct không thể được định nghĩa một khởi dựng (contructor) mà không có tham số
    * struct định nghĩa các contructor với các tham số phải định nghĩa chính xác tất cả các field bởi vì nó sẽ trả về điều khiển cho phương thức nào gọi nó

    Sử dụng một struct theo kiểu tự tạo thì có một tiện lợi là tiện dụng hơn trong việc xác định. Khi xác định các dãy của các lớp, trước hết bạn phải định nghĩa một dãy các reference. Sau đó, chương trình cần tương tác thông qua dãy này để tạo ra các thực thể của từng lớp. Việc chỉ xác định một dãy của các struct sẽ định vị mỗi struct khác nhau trong một block liên tiếp nhau. Như đã được đề cập ở trên. Mỗi kiểu đơn giản trong C# được định nghĩa là một struct để cung cấp cú pháp chuẩn pass-by-value mà các lập trình viên Java thường sử dụng.

    11.this và base

    Các đối tượng trong C# có thể tham khảo đến chính nó như trong Java. This mang cùng một nghĩa như thế nhưng C# sử dụng từ khóa base thay vì sử dụng từ khóa super như trong Java. Cả từ khóa this và base đều có thể sử dụng trong các phương thức và các contructor như this và super được sử dụng trong Java.

    12.Cài chồng phương thức (Overriding methods)

    Tất cả phương thức trong một đối tượng là “final” (một từ khóa trong Java) theo mặc định. Theo đó một phương thức được tải chồng bởi một đối tượng kế thừa, phương thức gốc cần phải được đánh dấu là “virtual” và tất cả các phương thức tải chồng phương thức virtual trong phương thức được kế thừa phải được đánh dấu “override”

    public class A : Object {
    public virtual void toOverride() { }
    }

    public class B : A {
    public override void toOverride() { }
    }


    13.Chuyển đổi kiểu

    Các lập trình viên Java thường chỉ thân thuộc với việc chuyển kiểu giữa các kiểu primitive và khi ép kiểu lên cao hơn cho siêu lớp và thấp hơn cho các lớp con. C# cho phép khả năng định nghĩa chuyển đổi kiểu tự tạo cho hai đối tượng bất kỳ. Hai kiểu chuyển đổi phải như sau:

    * Chuyển đổi tương đối: kiểu chuyển này yêu cầu kiểu đích phải được xác định trong phát biểu,cũng như việc chuyển đổi này không chắc chắn làm việc hoặc nếu nó làm việc thì kết quả của nó có thể bị mất đi thông tin. Các lập trình viên Java thường thân thuộc với việc chuyển đổi tuyệt đối khi ép một đối tượng thành một một đối tượng của các lớp con của nó.
    * Chuyển đổi tuyệt đối: việc chuyển đổi này không yêu cầu kiểu cha, cũng như việc chuyển đổi này chắc chắn làm việc.

    Dưới đây, chúng ta chuyển một kiểu double thành một kiểu FlooredDouble và ngược lại. Chúng ta cần định nghĩa việc chuyển kiểu tương đối từ double thành FlooredDouble, thông tin có thể bị mất đi trong quá trình chuyển đổi. Ở quá trình ngược lại, chúng ta sẽ định nghĩa việc chuyển kiểu tuyệt đối, và sẽ không bị mất thông tin.

    public class FloorDouble : Object{

    private double value;
    public FloorDouble( double value ) {
    this.value = Math.Floor( value );
    }

    public double Value {
    get {
    return this.value;
    }
    }

    public static explicit operator FloorDouble( double value ) {
    return new FloorDouble( value );
    }

    public static implicit operator double( FloorDouble value ) {
    return value.Value;
    }
    }

    // this class can be used by
    FloorDouble fl = (FloorDouble)10.5
    double d = fl;


    14.Tải chồng toán tử (Operator overloading)

    Tải chồng toán tử trong C# rất đơn giản. Lớp FlooredDouble ở trên có thể được thừa kế để chứa một phương thức static

    public static FloorDouble operator + ( FloorDouble fd1, FloorDouble fd2 ) {
    return new FloorDouble( fd1.Value + fd2.Value );
    }


    Và các phát biểu sau là đúng

    FloorDouble fd1 = new FloorDouble( 3.5 );
    FloorDouble fd2 = new FloorDouble( 4 );
    FloorDouble sum = fd1 + fd2;


    15.Tổ chức lại mã nguồn

    C# không đặt bất kỳ yêu cầu nào trong việc tổ chức file – một lập trình viên có thể sắp xếp toàn bộ chương trình C# bên trong một file .cs (Java thường yêu cầu một file .java chứa một lớp).
    C# cũng cung cấp một cách để chia nhỏ các đối tượng của chương trình tương tự như các khối trong Java. Sử dụng namespace, các kiểu có quan hệ có thể được nhóm vào trong một phân cấp. Nếu không có block “namespace” nó sẽ nằm trong một namespace mặc định.

    namespace com.oreilly {
    }


    Và tất cả các kiểu được định nghĩa trong block tồn tại trong phân cấp com.oreilly. Các phân cấp này có thể được lồng vào nhau

    namespace com {
    namespace oreilly {
    }
    }


    cho phép đoạn mã có thể được viết với những namespace lồng nhau.
    Để bổ sung một namespace, phải dùng từ khóa “using”. Các hàm cơ bản này cũng tương tự như phát biểu “import” trong Java, tuy nhiên dùng để bổ sung một lớp xác định. Using cũng có thể được dùng để ánh xạ các kiểu và các namespace. Ví dụ định nghĩa sau:

    namespace a.really.long.namespace {
    class theClass {
    }
    }


    có thể ánh xạ như sau:

    using short = a.really.long.namespace.theClass;

    16.Tổng kết

    Trong article này, không đề cập toàn bộ cú pháp của C# như mã không an toàn, xử lý lại… và các phát biểu khác. Thay vào đó, chúng ta nói đến một danh sách các phát biểu thân thuộc và tương ứng với những gì trong Java mà thôi.

    (Theo: OnDotNet)

  4. #4
    megafun.vn's Avatar
    megafun.vn vẫn chưa có mặt trong diễn đàn Búa Đá
    Tham gia
    Feb 2009
    Bài
    70
    Cảm ơn
    1
    Điểm
    45/22 bài viết
    VR power
    0

    Default

    Thích cả 2 nhưng thích C# hơn!

 

 

Quyền sử dụng

  • Bạn không thể gửi chủ đề mới
  • Bạn không thể gửi trả lời
  • Bạn không thể gửi file đính kèm
  • Bạn không thể tự sửa bài viết của mình
  •