Cafedev chào độc giả với chủ đề hôm nay: “Kotlin với Quy ước Viết mã.” Trong ngôn ngữ lập trình này, quy ước viết mã không chỉ là một tập hợp các nguyên tắc, mà là hướng dẫn giúp code trở nên sáng tạo và dễ hiểu. Từ cách khai báo hàm đến sử dụng coroutines, mọi điều đều được đặt ra một cách có ý nghĩa. Hãy cùng khám phá những quy ước này để viết mã Kotlin hiệu quả và dễ bảo trì.

Quy ước lập trình phổ biến và dễ theo dõi là rất quan trọng đối với bất kỳ ngôn ngữ lập trình nào. Ở đây, chúng tôi cung cấp hướng dẫn về kiểu mã và tổ chức mã cho các dự án sử dụng Kotlin.

Nội dung chính

1. Cấu hình kiểu mã trong IDE

Hai IDE phổ biến nhất cho Kotlin – IntelliJ IDEAAndroid Studio cung cấp hỗ trợ mạnh mẽ cho việc định dạng mã của bạn. Bạn có thể cấu hình chúng để tự động định dạng mã của bạn theo các định dạng mã đã cho.

1.1 Hướng dẫn áp dụng format mã Kotlin

  1. Điều hướng đến Settings/Preferences | Editor | Code Style | Kotlin.
  2. Nhấp Set from….
  3. Chọn Kotlin style guide.

1.2 Kiểm tra mã của bạn tuân theo các format mã của Kotlin

  1. Điều hướng đến Settings/Preferences | Editor | Inspections | General.
  2. Bật Incorrect formatting inspection. Các inspection bổ sung sẽ kiểm tra các vấn đề và mô tả trong hướng dẫn format mã (như quy ước đặt tên). Nó được kích hoạt theo mặc định.

2. Tổ chức mã nguồn

2.1 Cấu trúc thư mục

Trong các dự án Kotlin thuần, cấu trúc thư mục được khuyến nghị theo cấu trúc gói(package) với các gói cơ bản chung được bỏ qua. Ví dụ, nếu toàn bộ mã trong dự án thuộc gói org.example.kotlin và các gói con của nó, các tệp tin với gói org.example.kotlin nên được đặt trực tiếp dưới nguồn mã, và các tệp tin trong org.example.kotlin.network.socket nên ở trong thư mục con network/socket của nguồn mã.

Trên JVM: Trong các dự án sử dụng Kotlin cùng với Java, các tệp tin nguồn Kotlin nên nằm trong cùng nguồn mã như tệp tin nguồn Java và tuân theo cùng cấu trúc thư mục: mỗi tệp tin nên được lưu trữ trong thư mục tương ứng với mỗi câu lệnh gói(là câu lệnh import org.example.kotlin.network.socket kiểu này).

2.2 Tên tệp nguồn

Nếu một tệp tin Kotlin chỉ chứa một lớp hoặc giao diện (có thể kèm theo các khai báo cấp độ cao liên quan), tên của nó nên giống như tên của lớp, với phần mở rộng là .kt. Điều này áp dụng cho tất cả các loại lớp và giao diện. Nếu một tệp tin chứa nhiều lớp hoặc chỉ có các khai báo cấp độ cao, hãy chọn một tên mô tả nội dung của tệp tin và đặt tên cho tệp tin tương ứng. Sử dụng kiểu chữ in hoa đầu với chữ đầu tiên viết hoa (còn được gọi là Pascal case), ví dụ: ProcessDeclarations.kt.
Tên của tệp tin nên mô tả công việc mà mã trong tệp tin thực hiện. Do đó, bạn nên tránh sử dụng từ vô nghĩa như Util trong tên tệp tin.

Dự án đa nền tảng

Trong các dự án đa nền tảng, các tệp tin chứa các khai báo cấp độ cao trong các bộ nguồn cụ thể của nền tảng nên có một hậu tố liên quan đến tên của bộ nguồn. Ví dụ:
* jvmMain/kotlin/Platform.jvm.kt
* androidMain/kotlin/Platform.android.kt
* iosMain/kotlin/Platform.ios.kt
Đối với bộ nguồn chung, các tệp tin chứa các khai báo cấp độ cao không nên có một hậu tố. Ví dụ, commonMain/kotlin/Platform.kt.

Chi tiết kỹ thuật:

Chúng tôi khuyến khích tuân theo quy tắc đặt tên tệp tin này trong các dự án đa nền tảng do hạn chế của JVM: nó không cho phép các thành viên cấp độ cao (hàm, thuộc tính).
Để làm điều này, trình biên dịch Kotlin JVM tạo các lớp bọc (được gọi là “file facades”) chứa các khai báo thành viên cấp độ cao. File facades có tên nội bộ dựa trên tên tệp tin.
Lúc đó, JVM không cho phép nhiều lớp có cùng tên đầy đủ (FQN). Điều này có thể dẫn đến tình huống khi một dự án Kotlin không thể được biên dịch thành JVM:
noneroot
|- commonMain/kotlin/myPackage/Platform.kt // chứa 'fun count() { }'
|- jvmMain/kotlin/myPackage/Platform.kt // chứa 'fun multiply() { }'

Ở đây cả hai tệp tin Platform.kt đều ở trong cùng một gói, vì vậy trình biên dịch Kotlin JVM tạo ra hai file facade, cả hai đều có FQN myPackage.PlatformKt. Điều này gây ra lỗi Duplicate JVM classes.
Cách đơn giản nhất để tránh vấn đề này là đổi tên một trong hai tệp tin theo hướng dẫn ở trên. Quy tắc đặt tên này giúp tránh xung đột trong khi vẫn giữ được tính đọc mã.

Có hai trường hợp khi những khuyến nghị này có vẻ dư thừa, nhưng chúng tôi vẫn khuyến khích tuân theo:

* Các nền tảng không phải là JVM không gặp vấn đề với việc tạo các file facade trùng lặp. Tuy nhiên, quy tắc đặt tên này có thể giúp duy trì tính nhất quán trong đặt tên tệp tin.

* Trên JVM, nếu các tệp nguồn không có các khai báo cấp độ cao, các file facade không được tạo ra và bạn sẽ không gặp phải xung đột đặt tên. Tuy nhiên, quy tắc đặt tên này có thể giúp bạn tránh tình huống khi một sự tái cấu trúc đơn giản hoặc một bổ sung có thể bao gồm một hàm cấp độ cao và dẫn đến lỗi “Duplicate JVM classes”.

2.3 Tổ chức tệp nguồn

Đặt nhiều khai báo (lớp, hàm hoặc thuộc tính cấp độ cao) trong cùng một tệp nguồn Kotlin được khuyến khích miễn là những khai báo này liên quan chặt chẽ về mặt ngữ nghĩa và kích thước tệp vẫn hợp lý (không vượt quá vài trăm dòng).
Đặc biệt, khi định nghĩa các hàm mở rộng cho một lớp có liên quan đến tất cả khách hàng của lớp đó, đặt chúng trong cùng một tệp với lớp chính. Khi định nghĩa các hàm mở rộng chỉ có ý nghĩa đối với một khách hàng cụ thể, đặt chúng gần mã của khách hàng đó. Tránh tạo ra các tệp chỉ để chứa tất cả các hàm mở rộng của một lớp nào đó.

2.4 Bố cục lớp(Class)

Nội dung của một lớp nên đi theo thứ tự sau:
1. Khai báo thuộc tính và các khối khởi tạo

2. Các hàm xây dựng phụ

3. Khai báo phương thức

4. Đối tượng đồng minh

Không sắp xếp các khai báo phương thức theo thứ tự chữ cái hoặc theo khả năng nhìn thấy, và không tách các phương thức thông thường khỏi các phương thức mở rộng. Thay vào đó, đặt những thứ liên quan cùng nhau, để người đọc lớp từ đầu đến cuối có thể theo dõi logic của điều đang diễn ra. Chọn một thứ tự (hoặc thứ tự cấp cao trước, hoặc ngược lại) và tuân theo nó.
Đặt các lớp lồng vào gần mã sử dụng những lớp đó. Nếu các lớp dự định được sử dụng bên ngoài và không được tham chiếu bên trong lớp, đặt chúng vào cuối, sau đối tượng đồng minh.

2.5 Bố cục thực hiện giao diện

Khi thực hiện một giao diện, giữ cho các thành viên thực hiện ở cùng một thứ tự như thành viên của giao diện (nếu cần, xen kẽ với các phương thức riêng tư bổ sung được sử dụng cho việc thực hiện).

2.6 Bố cục của nạp chồng(Overload)

Luôn đặt các nạp chồng gần nhau trong một lớp.

3. Quy tắc đặt tên

Quy tắc đặt tên gói và lớp(package and class) trong Kotlin khá đơn giản:

  • Tên gói luôn là chữ thường và không sử dụng gạch dưới (org.example.project). Sử dụng tên có nhiều từ thông thường bị khuyến khích, nhưng nếu bạn cần sử dụng nhiều từ, bạn có thể đơn giản nối chúng lại hoặc sử dụng kiểu chữ camel (org.example.myProject).
  • Tên của lớp và đối tượng bắt đầu bằng một chữ cái viết hoa và sử dụng kiểu chữ camel:
open class DeclarationProcessor { /*...*/ }

object EmptyDeclarationProcessor : DeclarationProcessor() { /*...*/ }

3.1 Tên hàm

Tên của các hàm, thuộc tính và biến cục bộ bắt đầu bằng một chữ cái viết thường và sử dụng kiểu chữ camel và không sử dụng gạch dưới:

fun processDeclarations() { /*...*/ }
var declarationCount = 1

Ngoại trừ: các hàm nhà máy được sử dụng để tạo các thể hiện của lớp có thể có cùng tên với kiểu trả về trừu tượng:

interface Foo { /*...*/ }

class FooImpl : Foo { /*...*/ }

fun Foo(): Foo { return FooImpl() }

3.2 Tên cho các phương thức kiểm thử

Trong các bài kiểm thử (và chỉ trong các bài kiểm thử), bạn có thể sử dụng tên phương thức với dấu cách được bao quanh bởi dấu gạch cách ngược. Lưu ý rằng tên phương thức như vậy hiện không được hỗ trợ bởi runtime của Android. Việc sử dụng gạch dưới trong tên phương thức cũng được phép trong mã kiểm thử.

class MyTestCase {
@Test fun `ensure everything works`() { /*...*/ }

@Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}

3.3 Tên thuộc tính

Tên của các hằng số (các thuộc tính được đánh dấu bằng const, hoặc các thuộc tính val cấp cao hoặc thuộc tính val của đối tượng mà không có chức năng get tùy chỉnh giữ dữ liệu không thay đổi sâu) nên sử dụng tên viết hoa được phân cách bằng gạch dưới (screaming snake case):

const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"

Tên của các thuộc tính val cấp cao hoặc thuộc tính của đối tượng giữ các đối tượng có hành vi hoặc dữ liệu có thể thay đổi nên sử dụng tên kiểu chữ camel:

val mutableCollection: MutableSet = HashSet()

Tên của các thuộc tính giữ tham chiếu đến đối tượng độc thân có thể sử dụng cùng kiểu đặt tên như các khai báo object:

val PersonComparator: Comparator = /*...*/

Đối với các hằng số enum, việc sử dụng cả tên viết hoa được phân cách bằng gạch dưới (screaming snake case) (enum class Color { RED, GREEN }) hoặc tên kiểu chữ hoa cái đầu, tùy thuộc vào cách sử dụng, đều là chấp nhận.

3.4 Tên cho các thuộc tính sau lưng

Nếu một lớp có hai thuộc tính mà bản chất là giống nhau nhưng một là một phần của API công khai và một khác là chi tiết triển khai, hãy sử dụng gạch dưới làm tiền tố cho tên của thuộc tính riêng tư:

class C {
  private val _elementList = mutableListOf()

  val elementList: List
  get() = _elementList
}

3.5 Thế nào là chọn tên tốt?

  • Tên của một lớp thường là một danh từ hoặc cụm danh từ giải thích lớp là gì: List, PersonReader.
  • Tên của một phương thức thường là một động từ hoặc cụm động từ mô tả phương thức làm gì: close, readPersons. Tên cũng nên gợi ý nếu phương thức đang thay đổi đối tượng hay trả về một bản sao mới. Ví dụ, sort sắp xếp một bộ sưu tập ngay tại chỗ, trong khi sorted trả về một bản sao đã được sắp xếp của bộ sưu tập.
  • Tên nên làm rõ mục đích của thực thể là gì, vì vậy tốt nhất là tránh sử dụng từ vô nghĩa (Manager, Wrapper) trong các tên.
  • Khi sử dụng viết tắt là một phần của tên khai báo, viết hoa nếu nó bao gồm hai chữ cái (IOStream); viết hoa chỉ chữ đầu tiên nếu nó dài hơn (XmlFormatter, HttpInputStream).

4. Định dạng khi code với Kotlin

4.1 Lùi vào

Sử dụng bốn dấu cách để lùi vào. Không sử dụng tab.
Đối với dấu ngoặc nhọn, đặt dấu mở ngoặc ở cuối dòng nơi cấu trúc bắt đầu và dấu đóng ngoặc trên một dòng riêng được căn chỉnh theo chiều ngang với cấu trúc mở ngoặc.

if (elements != null) {
 for (element in elements) {
  // ...
 }
}

Trong Kotlin, dấu chấm phẩy là tùy chọn và do đó, các dòng xuống hàng có ý nghĩa quan trọng. Thiết kế ngôn ngữ giả định về kiểu dáng dấu ngoặc theo phong cách Java, và bạn có thể gặp phải hành vi đáng ngạc nhiên nếu bạn cố gắng sử dụng một kiểu định dạng khác.

4.2 Khoảng trắng ngang

  • Đặt khoảng trắng xung quanh các toán tử nhị phân (a + b). Ngoại lệ: không đặt khoảng trắng xung quanh toán tử “range to” (0..i).
  • Không đặt khoảng trắng xung quanh toán tử số học một ngôi (a++).
  • Đặt khoảng trắng giữa các từ khóa điều khiển luồng (if, when, for, và while) và dấu ngoặc mở tương ứng.
  • Không đặt khoảng trắng trước dấu ngoặc mở trong một khai báo constructor chính, khai báo phương thức hoặc gọi phương thức.

class A(val x: Int)

fun foo(x: Int) { ... }

fun bar() { foo(1) }

  • Không bao giờ đặt khoảng trắng sau (, [, hoặc trước ], )
  • Không bao giờ đặt khoảng trắng xung quanh . hoặc ?.: foo.bar().filter { it > 2 }.joinToString(), foo?.bar()
  • Đặt khoảng trắng sau //: // This is a comment
  • Không đặt khoảng trắng xung quanh dấu ngoặc nhọn được sử dụng để chỉ định các tham số kiểu: class Map<k, v=""> { ... }</k,>
  • Không đặt khoảng trắng xung quanh ::: Foo::class, String::length
  • Không đặt khoảng trắng trước ? được sử dụng để đánh dấu một kiểu có thể null: String?

Như một quy tắc chung, tránh sự căn chỉnh ngang bất kỳ loại nào. Việc đổi tên một định danh thành một tên có chiều dài khác không nên ảnh hưởng đến định dạng của cả khai báo và bất kỳ sử dụng nào.

4.3 Dấu hai chấm

Đặt một khoảng trắng trước : trong các trường hợp sau:
* khi nó được sử dụng để phân tách giữa một kiểu và một kiểu cha
* khi chuyển gọi đến một constructor của lớp cha hoặc một constructor khác của cùng một lớp
* sau từ khóa object
Không đặt khoảng trắng trước : khi nó phân tách một khai báo và kiểu của nó.
Luôn đặt một khoảng trắng sau :.

abstract class Foo : IFoo {
 abstract fun foo(a: Int): T
}

class FooImpl : Foo() {
 constructor(x: String) : this(x) { /*...*/ }

 val x = object : IFoo { /*...*/ }
}

4.4 Phần đầu của lớp

Các lớp với một số tham số constructor chính có thể được viết trong một dòng duy nhất:

class Person(id: Int, name: String)

Các lớp có tiêu đề dài hơn nên được định dạng sao cho mỗi tham số constructor chính ở trên một dòng riêng biệt với lùi vào. Ngoài ra, dấu đóng ngoặc nên ở một dòng mới. Nếu bạn sử dụng kế thừa, cuộc gọi constructor của lớp cha, hoặc danh sách các giao diện được triển khai nên ở cùng một dòng như dấu ngoặc:

class Person(
  id: Int,
  name: String,
  surname: String
) : Human(id, name) { /*...*/ }

Đối với nhiều giao diện, cuộc gọi constructor của lớp cha nên được đặt trước và sau đó mỗi giao diện nên được đặt trên một dòng khác nhau:

class Person(
  id: Int,
  name: String,
  surname: String
) : Human(id, name),
KotlinMaker { /*...*/ }

Đối với các lớp có danh sách kiểu cha dài, hãy đặt một dòng xuống sau dấu hai chấm và căn chỉnh tất cả tên kiểu cha theo chiều ngang:

class MyFavouriteVeryLongClassHolder :
  MyLongHolder(),
  SomeOtherInterface,
  AndAnotherOne {

fun foo() { /*...*/ }
}

Để rõ ràng phân biệt đầu lớp và thân khi đầu lớp dài, bạn có thể đặt một dòng trắng sau đầu lớp (như trong ví dụ trên), hoặc đặt dấu ngoặc nhọn mở trên một dòng riêng biệt:

class MyFavouriteVeryLongClassHolder :
  MyLongHolder(),
  SomeOtherInterface,
  AndAnotherOne
{
fun foo() { /*...*/ }
}

Sử dụng lùi vào bình thường (bốn dấu cách) cho các tham số constructor. Điều này đảm bảo rằng các thuộc tính được khai báo trong constructor chính có cùng lùi vào như các thuộc tính được khai báo trong thân lớp.

4.5 Thứ tự khai báo các loại Modifiers

Nếu một khai báo có nhiều modifiers, luôn đặt chúng theo thứ tự sau:

public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation / fun // as a modifier in `fun interface`
companion
inline / value
infix
operator
data

Đặt tất cả các annotations trước modifiers:

@Named("Foo")
private val foo: Foo

Trừ khi bạn đang làm việc trên một thư viện, hãy bỏ qua các modifiers dư thừa (ví dụ: public).

4.6 Annotations

Đặt annotations trên các dòng riêng biệt trước khai báo mà chúng được gắn liền, và với cùng một lùi vào:

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

Annotations không có tham số có thể được đặt trên cùng một dòng:

@JsonExclude @JvmField
var x: String

Một single annotation không có tham số có thể được đặt trên cùng một dòng với khai báo tương ứng:

@Test fun foo() { /*...*/ }

4.7 File annotations

File annotations được đặt sau comment của file (nếu có), trước câu lệnh package,và được ngăn cách với package bằng một dòng trống (để nhấn mạnh rằng chúng nhắm vào file và không phải là package).

/** License, copyright and whatever */
@file:JvmName("FooBar")

package foo.bar

4.8 Functions

Nếu tên của hàm không fit or dài trong một dòng, sử dụng cú pháp sau:

fun longMethodName(
  argument: ArgumentType = defaultValue,
  argument2: AnotherArgumentType,
): ReturnType {
   // body
}

Sử dụng lùi vào bình thường (bốn dấu cách) cho tham số của hàm. Điều này giúp đảm bảo tính nhất quán với các tham số constructor.
Ưu tiên sử dụng expression body cho các hàm với body chỉ gồm một biểu thức đơn.

fun foo(): Int { // bad
  return 1
}

fun foo() = 1 // good

4.9 Hàm có body ngắn gọn

Nếu hàm có expression body mà dòng đầu tiên không fit trên cùng một dòng với khai báo, đặt ký tự = trên dòng đầu tiên và lùi vào expression body bằng bốn dấu cách.

fun f(x: String, y: String, z: String) =
veryLongFunctionCallWithManyWords(andLongParametersToo(), x, y, z)

4.10 Properties(thuộc tính)

Đối với các thuộc tính chỉ đọc đơn giản, xem xét định dạng trên một dòng:

val isEmpty: Boolean get() = size == 0

Đối với các thuộc tính phức tạp hơn, luôn đặt từ khóa getset trên các dòng riêng biệt:

val foo: String
  get() { /*...*/ }

Đối với các thuộc tính có initializer, nếu initializer dài, thêm một dòng xuống sau ký tự = và lùi vào initializer bằng bốn dấu cách:

private val defaultCharset: Charset? =
   EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

4.11 Câu lệnh điều khiển

Nếu điều kiện của một câu lệnh if hoặc when trải dài qua nhiều dòng, luôn sử dụng dấu ngoặc nhọn xung quanh phần thân của câu lệnh. Lùi vào mỗi dòng tiếp theo của điều kiện bằng bốn dấu cách so với bắt đầu câu lệnh. Đặt ngoặc đóng của điều kiện cùng với dấu ngoặc nhọn mở trên một dòng riêng biệt:

if (!component.isSyncing &&
  !hasAnyKotlinRuntimeInScope(module)
) {
   return createKotlinNotConfiguredPanel(module)
}

Điều này giúp căn chỉnh điều kiện và thân câu lệnh.
Đặt các từ khóa else, catch, finally, cũng như từ khóa while của vòng lặp do-while, trên cùng một dòng với dấu ngoặc nhọn trước đó:

if (condition) {
   // body
} else {
   // else part
}

try {
  // body
} finally {
  // cleanup
}

Trong một câu lệnh when, nếu một nhánh dài hơn một dòng, xem xét việc tách nó khỏi các khối case kề cận bằng một dòng trắng:

private fun parsePropertyValue(propName: String, token: Token) {
   when (token) {
      is Token.ValueToken ->
         callback.visitValue(propName, token.value)

      Token.LBRACE -> { // ...
     }
   }
}

Đặt các nhánh ngắn trên cùng một dòng với điều kiện, mà không cần dấu ngoặc nhọn.

when (foo) {
    true -> bar() // good
    false -> { baz() } // bad
}

4.12 Gọi phương thức

Trong danh sách đối số dài, thêm dòng xuống sau dấu ngoặc mở. Lùi vào các đối số bằng bốn dấu cách. Nhóm nhiều đối số liên quan chặt chẽ trên cùng một dòng.

drawSquare(
    x = 10, y = 10,
    width = 100, height = 100,
    fill = true
)

Đặt khoảng trắng xung quanh dấu = phân tách tên đối số và giá trị.

4.13 Bọc gọi phương thức liên tiếp

Khi bọc các gọi phương thức liên tiếp, đặt dấu . hoặc toán tử ?. trên dòng tiếp theo, với một lùi vào duy nhất:

val anchor = owner
  ?.firstChild!!
  .siblings(forward = true)
  .dropWhile { it is PsiComment || it is PsiWhiteSpace }

Thường thì, cuộc gọi đầu tiên trong chuỗi nên có một dòng trước đó, nhưng nếu mã nguồn trở nên rõ ràng hơn khi không có nó, có thể bỏ qua.

4.14 Lambdas

Trong biểu thức lambda, khoảng trắng nên được sử dụng xung quanh dấu ngoặc nhọn, cũng như xung quanh dấu mũi tên phân tách tham số và thân. Nếu một cuộc gọi lấy một lambda duy nhất, hãy truyền nó bên ngoài dấu ngoặc đơn nếu có thể.

list.filter { it > 10 }

Nếu gán một nhãn cho một lambda, không đặt khoảng trắng giữa nhãn và dấu ngoặc nhọn mở:

fun foo() {
  ints.forEach lit@{
  // ...
  }
}

Khi khai báo tên tham số trong một lambda trải dài qua nhiều dòng, đặt tên ở dòng đầu tiên, tiếp theo là mũi tên và dòng mới:

appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj) // ...
}

Nếu danh sách tham số quá dài để chứa trên một dòng, đặt mũi tên trên một dòng riêng biệt:

foo {
  context: Context,
  environment: Env
  ->
  context.configureEnv(environment)
}

4.15 Dấu phẩy cuối cùng

Dấu phẩy cuối cùng là ký hiệu phẩy sau mục cuối cùng trong một loạt các phần tử:

class Person(
  val firstName: String,
  val lastName: String,
  val age: Int, // trailing comma
)

Việc sử dụng dấu phẩy cuối cùng mang lại một số lợi ích:
* Làm cho sự khác biệt trong việc kiểm soát phiên bản trở nên sạch sẽ – vì tất cả sự chú ý đều tập trung vào giá trị đã thay đổi.
* Dễ dàng thêm và sắp xếp các phần tử – không cần phải thêm hoặc xóa dấu phẩy khi bạn thao tác các phần tử.
* Đơn giản hóa việc tạo mã, ví dụ: cho các khởi tạo đối tượng. Phần tử cuối cũng có thể có dấu phẩy.
Dấu phẩy cuối cùng là hoàn toàn tùy chọn – mã của bạn vẫn sẽ hoạt động mà không cần chúng. Hướng dẫn kiểu Kotlin khuyến khích việc sử dụng dấu phẩy cuối cùng tại điểm khai báo và để lại quyết định đó cho bạn tại điểm gọi.

Để kích hoạt dấu phẩy cuối cùng trong bộ format của IntelliJ IDEA, điều hướng đến Settings/Preferences | Editor | Code Style | Kotlin,
mở tab Other và chọn tùy chọn Use trailing comma.

Enumerations

enum class Direction {
  NORTH,
  SOUTH,
  WEST,
  EAST, // trailing comma
}

Value arguments

fun shift(x: Int, y: Int) { /*...*/ }
shift(
    25,
    20, // trailing comma
)
val colors = listOf(
    "red",
    "green",
    "blue", // trailing comma
)

Class properties and parameters

class Customer(
    val name: String,
    val lastName: String, // trailing comma
)
class Customer(
    val name: String,
    lastName: String, // trailing comma
)

Function value parameters

fun powerOf(
    number: Int,
    exponent: Int, // trailing comma
) { /*...*/ }
constructor(
    x: Comparable<Number>,
    y: Iterable<Number>, // trailing comma
) {}
fun print(
    vararg quantity: Int,
    description: String, // trailing comma
) {}

Parameters with optional type (including setters)

val sum: (Int, Int, Int) -> Int = fun(
    x,
    y,
    z, // trailing comma
): Int {
    return x + y + x
}
println(sum(8, 8, 8))

Indexing suffix

class Surface {
    operator fun get(x: Int, y: Int) = 2 * x + 4 * y - 10
}
fun getZValue(mySurface: Surface, xValue: Int, yValue: Int) =
    mySurface[
        xValue,
        yValue, // trailing comma
    ]

Parameters in lambdas

fun main() {
    val x = {
            x: Comparable<Number>,
            y: Iterable<Number>, // trailing comma
        ->
        println("1")
    }
    println(x)
}

when entry

fun isReferenceApplicable(myReference: KClass<*>) = when (myReference) {
    Comparable::class,
    Iterable::class,
    String::class, // trailing comma
        -> true
    else -> false
}

Collection literals

annotation class ApplicableFor(val services: Array<String>)
@ApplicableFor([
    "serializer",
    "balancer",
    "database",
    "inMemoryCache", // trailing comma
])
fun run() {}

Type arguments

fun <T1, T2> foo() {}
fun main() {
    foo<
            Comparable<Number>,
            Iterable<Number>, // trailing comma
            >()
}

Type parameters

class MyMap<
        MyKey,
        MyValue, // trailing comma
        > {}

Destructuring declarations

data class Car(val manufacturer: String, val model: String, val year: Int)
val myCar = Car("Tesla", "Y", 2019)
val (
    manufacturer,
    model,
    year, // trailing comma
) = myCar
val cars = listOf<Car>()
fun printMeanValue() {
    var meanValue: Int = 0
    for ((
        _,
        _,
        year, // trailing comma
    ) in cars) {
        meanValue += year
    }
    println(meanValue/cars.size)
}
printMeanValue()

5. Ghi chú tài liệu(comments)

Đối với các ghi chú tài liệu dài hơn, hãy đặt /** ở một dòng riêng và bắt đầu mỗi dòng tiếp theo bằng một dấu sao:

/**
* This is a documentation comment
* on multiple lines.
*/

Ghi chú ngắn có thể được đặt trên một dòng:

/** This is a short documentation comment. */

Nói chung, hạn chế việc sử dụng các thẻ @param@return. Thay vào đó, tích hợp mô tả của tham số và giá trị trả về trực tiếp vào ghi chú tài liệu, và thêm liên kết đến các tham số mỗi khi được đề cập. Sử dụng @param@return chỉ khi cần một mô tả dài không phù hợp với luồng chính của văn bản chính.

// Avoid doing this:

/**
 * Returns the absolute value of the given number.
 * @param number The number to return the absolute value for.
 * @return The absolute value.
 */
fun abs(number: Int): Int { /*...*/ }

// Do this instead:

/**
 * Returns the absolute value of the given [number].
 */
fun abs(number: Int): Int { /*...*/ }

6. Tránh các cấu trúc dư thừa

Nói chung, nếu một cấu trúc ngữ pháp cụ thể trong Kotlin là tùy chọn và được IDE đánh dấu là dư thừa, hãy bỏ nó trong mã của bạn. Không để lại các phần ngữ pháp không cần thiết trong mã chỉ “vì rõ ràng”.

6.1 Loại trả về Unit

Nếu một hàm trả về Unit, loại trả về nên được bỏ qua:

fun foo() { // ": Unit" is omitted here

}

6.2 Dấu chấm phẩy

Bỏ qua dấu chấm phẩy bất cứ khi nào có thể.

6.3 Mẫu chuỗi

Bỏ qua dấu chấm phẩy khi có thể.

Không sử dụng dấu ngoặc nhọn khi chèn một biến đơn vào mẫu chuỗi. Sử dụng dấu ngoặc nhọn chỉ cho các biểu thức dài hơn.

println("$name has ${children.size} children")

7. Sử dụng ngôn ngữ một cách tự nhiên

7.1 Thuộc tính bất biến

Ưu tiên sử dụng dữ liệu không thay đổi thay vì có thể thay đổi. Luôn khai báo biến cục bộ và thuộc tính như val thay vì var nếu chúng không được sửa đổi sau khởi tạo.
Luôn sử dụng giao diện bộ sưu tập không thay đổi (Collection, List, Set, Map) để khai báo các bộ sưu tập không biến. Khi sử dụng các hàm tạo để tạo các thể hiện bộ sưu tập, hãy luôn sử dụng các hàm trả về loại bộ sưu tập không thay đổi nếu có thể:

// Bad: use of a mutable collection type for value which will not be mutated
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// Good: immutable collection type used instead
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

// Bad: arrayListOf() returns ArrayList<T>, which is a mutable collection type
val allowedValues = arrayListOf("a", "b", "c")

// Good: listOf() returns List<T>
val allowedValues = listOf("a", "b", "c")

7.2 Giá trị mặc định của tham số

Ưu tiên khai báo các hàm với giá trị mặc định của tham số thay vì khai báo các hàm nạp chồng.

// Bad
fun foo() = foo("a")
fun foo(a: String) { /*...*/ }

// Good
fun foo(a: String = "a") { /*...*/ }

7.3 kiểu Bí danh

Nếu bạn có một loại hàm hoặc một loại với tham số kiểu được sử dụng nhiều lần trong mã nguồn, ưu tiên định nghĩa một bí danh kiểu cho nó:

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<string, person="">

Nếu bạn sử dụng một bí danh kiểu riêng tư hoặc nội bộ để tránh xung đột tên, hãy ưu tiên cú pháp import ... as ... được đề cập trong Gói và Nhập.</string,>

7.4 Tham số lambda

Trong lambda ngắn và không lồng, nên sử dụng quy ước it thay vì khai báo tham số một cách rõ ràng. Trong lambda lồng với tham số, luôn khai báo tham số một cách rõ ràng.

7.5 Trả về trong lambda

Tránh sử dụng nhiều trả về được đặt nhãn trong lambda. Xem xét cấu trúc lại lambda sao cho nó có một điểm thoát duy nhất. Nếu điều đó không khả thi hoặc không đủ rõ ràng, xem xét chuyển lambda thành hàm ẩn danh.
Không sử dụng trả về được đặt nhãn cho câu lệnh cuối cùng trong lambda.

7.6 Đối số có tên

Sử dụng cú pháp đối số có tên khi một phương thức chấp nhận nhiều tham số cùng kiểu nguyên thủy hoặc cho các tham số kiểu Boolean, trừ khi ý nghĩa của tất cả các tham số hoàn toàn rõ ràng từ ngữ cảnh.

drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)

7.7 Câu lệnh điều kiện

Ưu tiên sử dụng biểu thức của try, if, và when.

return if (x) foo() else bar()
return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

Thích hơn là:

if (x)
    return foo()
else
    return bar()
when(x) {
    0 -> return "zero"
    else -> return "nonzero"
}

7.8 if so với when

Ưu tiên sử dụng if cho điều kiện nhị phân thay vì when. Ví dụ, sử dụng cú pháp này với if:

if (x == null) … else …

Thay vì cú pháp này với when:

when (x) {
    null -> // ...
    else -> // ...
}

Ưu tiên sử dụng when nếu có ba lựa chọn trở lên.

7.9 Giá trị Boolean có thể là null trong điều kiện

Nếu bạn cần sử dụng giá trị Boolean có thể là null trong một câu lệnh điều kiện, sử dụng kiểm tra if (value == true) hoặc if (value == false).

7.10 Vòng lặp

Ưu tiên sử dụng các hàm bậc cao (filter, map v.v.) thay vì vòng lặp. Ngoại lệ: forEach (ưu tiên sử dụng một vòng lặp for thông thường thay vì, trừ khi người nhận của forEach có thể là null hoặc forEach được sử dụng như một phần của chuỗi gọi dài).
Khi đưa ra quyết định giữa một biểu thức phức tạp sử dụng nhiều hàm bậc cao và một vòng lặp, hiểu rõ chi phí của các thao tác được thực hiện trong mỗi trường hợp và luôn lưu ý đến yếu tố hiệu suất.

7.11 Vòng lặp trên các khoảng

Sử dụng toán tử ..< để lặp qua một khoảng không giới hạn:

for (i in 0..n - 1) { /*...*/ } // bad
for (i in 0..<n) *...*="" <="" good="" p="" {="" }="">

7.12 Chuỗi

Ưu tiên sử dụng mẫu chuỗi thay vì nối chuỗi.
Ưu tiên sử dụng chuỗi đa dòng thay vì nhúng các chuỗi thoát dòng \n vào trong chuỗi thông thường.
Để duy trì thụt đầu dòng trong chuỗi đa dòng, sử dụng trimIndent khi chuỗi kết quả không yêu cầu bất kỳ thụt đầu dòng nội bộ nào, hoặc trimMargin khi thụt đầu dòng nội bộ được yêu cầu:

println("""
    Not
    trimmed
    text
    """
       )

println("""
    Trimmed
    text
    """.trimIndent()
       )

println()

val a = """Trimmed to margin text:
          |if(a > 1) {
          |    return a
          |}""".trimMargin()

println(a)

Tìm hiểu sự khác biệt giữa chuỗi đa dòng trong Java và Kotlin.

7.13 Hàm so với thuộc tính

Trong một số trường hợp, hàm không có đối số có thể hoán đổi được với thuộc tính chỉ đọc. Mặc dù ý nghĩa tương tự, nhưng có một số quy ước về phong cách khi nào nên ưu tiên cái này hơn cái kia.
Ưu tiên thuộc tính hơn hàm khi giải thuật cơ bản:
* không gây ra lỗi
* tính toán rẻ (hoặc được lưu vào bộ nhớ cache khi chạy lần đầu)
* trả về kết quả giống nhau qua các lời gọi nếu trạng thái của đối tượng không thay đổi

7.14 Hàm mở rộng

Sử dụng hàm mở rộng một cách rộng rãi. Mỗi khi bạn có một hàm hoạt động chủ yếu trên một đối tượng, hãy xem xét việc biến nó thành một hàm mở rộng chấp nhận đối tượng đó làm người nhận. Để giảm thiểu ô nhiễm API, hãy hạn chế khả năng nhìn thấy của các hàm mở rộng theo mức có ý nghĩa. Theo cần, sử dụng các hàm mở rộng cục bộ, hàm mở rộng thành viên, hoặc hàm mở rộng cấp cao với khả năng nhìn thấy riêng.

7.15 Hàm trung gian

Khai báo một hàm là infix chỉ khi nó hoạt động trên hai đối tượng có vai trò tương tự. Ví dụ tốt: and, to, zip. Ví dụ xấu: add.
Đừng khai báo một phương thức là infix nếu nó biến đổi đối tượng nhận.

7.16 Hàm Factory

Nếu bạn khai báo một hàm nhà máy cho một lớp, hãy tránh đặt tên giống với tên của lớp đó. Ưu tiên sử dụng một tên khác biệt, làm cho rõ ràng tại sao hành vi của hàm nhà máy là đặc biệt. Chỉ khi thực sự không có ngữ nghĩa đặc biệt, bạn có thể sử dụng cùng tên với lớp.

class Point(val x: Double, val y: Double) {
    companion object {
        fun fromPolar(angle: Double, radius: Double) = Point(...)
    }
}

Nếu bạn có một đối tượng với nhiều hàm tạo quá tải không gọi các hàm tạo của lớp cơ sở khác nhau và không thể giảm xuống thành một hàm tạo duy nhất với giá trị mặc định, ưu tiên thay thế các hàm tạo quá tải bằng các hàm nhà máy.

7.17 Kiểu nền tảng

Một hàm/phương thức công khai trả về một biểu thức của kiểu nền tảng phải khai báo kiểu Kotlin của nó một cách rõ ràng:

fun apiCall(): String = MyJavaApi.getProperty("name")

Bất kỳ thuộc tính nào (cấp độ gói hoặc cấp độ lớp) được khởi tạo bằng một biểu thức của kiểu nền tảng phải khai báo kiểu Kotlin của nó một cách rõ ràng:

class Person {
  val name: String = MyJavaApi.getProperty("name")
}

Một giá trị cục bộ được khởi tạo bằng một biểu thức của kiểu nền tảng có thể có hoặc không có khai báo kiểu:

class Person {
    val name: String = MyJavaApi.getProperty("name")
}

7.18 Hàm phạm vi apply/with/run/also/let

Kotlin cung cấp một bộ các hàm để thực thi một khối mã trong ngữ cảnh của một đối tượng cụ thể: let, run, with, apply, và also. Đối với hướng dẫn chọn hàm phạm vi phù hợp với trường hợp của bạn, xem Các Hàm Phạm Vi.

8. Quy ước viết mã cho thư viện

Khi viết thư viện, nên tuân theo một bộ quy tắc bổ sung để đảm bảo tính ổn định của API:

  • Luôn rõ ràng chỉ định tính hiển thị của thành viên (để tránh vô tình tiết lộ các khai báo như API công kha
  • Luôn rõ ràng chỉ định kiểu trả về của hàm và kiểu thuộc tính (để tránh thay đổi kiểu trả về khi triển khai thay đổi)
  • Cung cấp KDoc cho tất cả các thành viên công khai, ngoại trừ những lớp ghi đè không cần bất kỳ tài liệu mới(nhằm hỗ trợ tạo tài liệu cho thư viện)

Chân thành cảm ơn bạn đã đọc bài chia sẻ về “Kotlin với Quy ước Viết mã” trên Cafedev. Hy vọng rằng thông tin đã giúp bạn hiểu rõ hơn về cách tối ưu hóa việc viết mã trong Kotlin. Cùng Cafedev đồng hành và khám phá thêm nhiều điều thú vị về lập trình và công nghệ. Đừng ngần ngại chia sẻ ý kiến và thắc mắc của bạn. Hãy tiếp tục đọc và học hỏi tại Cafedev, nơi gặp gỡ cộng đồng đam mê công nghệ!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!