- Introduction
- Part 1 Good Code
- Chapter 1 Safety
- 引言
- 第1条:限制可变性
- 第2条:最小化变量作用域
- 第3条:尽快消除平台类型
- 第4条:不要把推断类型暴露给外部
- Item 5 Specify Your Expectations On Arguments And State
- 第6条:尽可能使用标准库中提供的异常
- 第7条:当不能返回预期结果时,优先使用null o或Failure 作为返回值
- Item 8 Handle Nulls Properly
- 第9条:使用use关闭资源
- Item 10 Write Unit Tests
- Chapter 2 Readability
- Introduction
- Item 11 Design For Readability
- Item 12 Operator Meaning Should Be Consistent With Its Function Name
- Item 13 Avoid Returning Or Operating On Unit
- Item 14 Specify The Variable Type When It Is Not Clear
- Item 15 Consider Referencing Receivers Explicitly
- Item 16 Properties Should Represent State Not Behavior
- Item 17 Consider Naming Arguments
- Item 18 Respect Coding Conventions
- Part 2 Code Design
- Chapter 3 Reusability
- Introduction
- Item 19 Do Not Repeat Knowledge
- Item 20 Do Not Repeat Common Algorithms
- Item 21 Use Property Delegation To Extract Common Property Patterns
- Item 22 Use Generics When Implementing Common Algorithms
- Item 23 Avoid Shadowing Type Parameters
- Item 24 Consider Variance For Generic Types
- Item 25 Reuse Between Different Platforms By Extracting Common Modules
- Chapter 4 Abstraction Design
- Introduction
- Item 26 Each Function Should Be Written In Terms Of A Single Level Of Abstraction
- Item 27 Use Abstraction To Protect Code Against Changes
- Item 28 Specify API Stability
- Item 29 Consider Wrapping External API
- Item 30 Minimize Elements Visibility
- Item 31 Define Contract With Documentation
- Item 32 Respect Abstraction Contracts
- Chapter 5 Object Creation
- Introduction
- Item 33 Consider Factory Functions Instead Of Constructors
- Item 34 Consider A Primary Constructor With Named Optional Arguments
- Item 35 Consider Defining A DSL For Complex Object Creation
- Chapter 6 Class Design
- Introduction
- Item 36 Prefer Composition Over Inheritance
- Item 37 Use The Data Modifier To Represent A Bundle Of Data
- Item 38 Use Function Types Instead Of Interfaces To Pass Operations And Actions
- Item 39 Prefer Class Hierarchies To Tagged Classes
- Item 40 Respect The Contract Of Equals
- Item 41 Respect The Contract Of Hash Code
- Item 42 Respect The Contract Of Compare To
- Item 43 Consider Extracting Non Essential Parts Of Your API Into Extensions
- Item 44 Avoid Member Extensions
- Part 3 Efficiency
- Chapter 7 Make It Cheap
- Introduction
- Item 45 Avoid Unnecessary Object Creation
- Item 46 Use Inline Modifier For Functions With Parameters Of Functional Types
- Item 47 Consider Using Inline Classes
- Item 48 Eliminate Obsolete Object References
- Chapter 8 Efficient Collection Processing
- Introduction
- Item 49 Prefer Sequence For Big Collections With More Than One Processing Step
- Item 50 Limit The Number Of Operations
- Item 51 Consider Arrays With Primitives For Performance Critical Processing
- Item 52 Consider Using Mutable Collections
- Published with GitBook
Item 17 Consider Naming Arguments
Item 17: Consider naming arguments
When you read a code, it is not always clear what an argument means. Take a look at the following example:
val text = (1..10).joinToString("|")
What is "|"
? If you know joinToString
well, you know that it is the separator
. Although it could just as well be the prefix
. It is not clear at all3. We can make it easier to read by clarifying those arguments whose values do not clearly indicate what they mean. The best way to do that is by using named arguments:
val text = (1..10).joinToString(separator = "|")
We could achieve a similar result by naming variable:
val separator = "|"
val text = (1..10).joinToString(separator)
Although naming the argument is more reliable. A variable name specifies developer intention, but not necessarily correctness. What if a developer made a mistake and placed the variable in the wrong position? What if the order of parameters changed? Named arguments protect us from such situations while named values do not. This is why it is still reasonable to use named arguments when we have values named anyway:
val separator = "|"
val text = (1..10).joinToString(separator = separator)
When should we use named arguments?
Clearly, named arguments are longer, but they have two important advantages:
- Name that indicates what value is expected.
- They are safer because they are independent of order.
The argument name is important information not only for a developer using this function but also for the one reading how it was used. Take a look at this call:
sleep(100)
How much will it sleep? 100 ms? Maybe 100 seconds? We can clarify it using a named argument:
sleep(timeMillis = 100)
This is not the only option for clarification in this case. In statically typed languages like Kotlin, the first mechanism that protects us when we pass arguments is the parameter type. We could use it here to express information about time unit:
sleep(Millis(100))
Or we could use an extension property to create a DSL-like syntax:
sleep(100.ms)
Types are a good way to pass such information. If you are concerned about efficiency, use inline classes as described in Item 46: Use inline modifier for functions with parameters of functional types. They help us with parameter safety, but they do not solve all problems. Some arguments might still be unclear. Some arguments might still be placed on wrong positions. This is why I still suggest considering named arguments, especially for parameters:
- with default arguments,
- with the same type as other parameters,
- of functional type, if they’re not the last parameter.
Parameters with default arguments
When a property has a default argument, we should nearly always use it by name. Such optional parameters are changed more often than those that are required. We don’t want to miss such a change. Function name generally indicates what are its non-optional arguments, but not what are its optional ones. This is why it is safer and generally cleaner to name optional arguments.4
Many parameters with the same type
As we said, when parameters have different types, we are generally safe from placing an argument at an incorrect position. There is no such freedom when some parameters have the same type.
fun sendEmail(to: String, message: String) { /*...*/ }
With a function like this, it is good to clarify arguments using names:
sendEmail(
to = "contact@kt.academy",
message = "Hello, ..."
)
Parameters of function type
Finally, we should treat parameters with function types specially. There is one special position for such parameters in Kotlin: the last position. Sometimes a function name describes an argument of a function type. For instance, when we see repeat
, we expect that a lambda after that is the block of code that should be repeated. When you see thread
, it is intuitive that the block after that is the body of this new thread. Such names only describe the function used at the last position.
thread {
// ...
}
All other arguments with function types should be named because it is easy to misinterpret them. For instance, take a look at this simple view DSL:
val view = linearLayout {
text("Click below")
button({ /* 1 */ }, { /* 2 */ })
}
Which function is a part of this builder and which one is an on-click listener? We should clarify it by naming the listener and moving builder outside of arguments:
val view = linearLayout {
text("Click below")
button(onClick = { /* 1 */ }) {
/* 2 */
}
}
Multiple optional arguments of a function type can be especially confusing:
fun call(before: ()->Unit = {}, after: ()->Unit = {}){
before()
print("Middle")
after()
}
call({ print("CALL") }) // CALLMiddle
call { print("CALL") } // MiddleCALL
To prevent such situations, when there is no single argument of a function type with special meaning, name them all:
call(before = { print("CALL") }) // CALLMiddle
call(after = { print("CALL") }) // MiddleCALL
This is especially true for reactive libraries. For instance, in RxJava when we subscribe to an Observable
, we can set functions that should be called:
- on every received item
- in case of error,
- after the observable is finished.
In Java I’ve often seen people setting them up using lambda expressions, and specifying in comments which method each lambda expression is:
// Java
observable.getUsers()
.subscribe((List<User> users) -> { // onNext
// ...
}, (Throwable throwable) -> { // onError
// ...
}, () -> { // onCompleted
// ...
});
In Kotlin we can make a step forward and use named arguments instead:
observable.getUsers()
.subscribeBy(
onNext = { users: List<User> ->
// ...
},
onError = { throwable: Throwable ->
// ...
},
onCompleted = {
// ...
})
Notice that I changed function name from subscribe
to subscribeBy
. It is because RxJava
is written in Java and we cannot use named arguments when we call Java functions. It is because Java does not preserve information about function names. To be able to use named arguments we often need to make our Kotlin wrappers for those functions (extension functions that are alternatives to those functions).
Summary
Named arguments are not only useful when we need to skip some default values. They are important information for developers reading our code, and they can improve the safety of our code. We should consider them especially when we have more parameters with the same type (or with functional types) and for optional arguments. When we have multiple parameters with functional types, they should almost always be named. An exception is the last function argument when it has a special meaning, like in DSL.