Kotlin Learning (X) : Other Kotlin techniques
Data deconstruction
Data deconstruction is the resolution of the data in the object into the corresponding independent variables next door, that is, separate from the original object and exist.
var (name, age) = user
Copy the code
In this line of code, the name and age attributes of the User object are deconstructed and assigned to the name and age variables, respectively. To do so, the User object needs to be an instance of the data class.
data class User(var name: String, var age: Int)
Copy the code
This line of code is a User data class. This data class has two parameters. The code below main assigns the corresponding attribute values of these two parameters to the corresponding variables.
var user = User("Mike".23)
var (name, age) = user
println("name = $name , age = $age") Name = Mike, age = 23
Copy the code
If you want a function to return multiple values and be able to deconstruct those values, you also need to return data-like objects.
fun printUser(a): User {
return User("Jack".20)}fun main(args: Array<String>) {
var (name, age) = printUser()
println("name = $name , age = $age") // Result: name = Jack, age = 20
}
Copy the code
There are many objects, can hold a set of values, Wells, and can pass for… In English, deconstruct these values. For example, Map objects do this. The following code creates a MutableMap object, saves two key-value pairs, and then uses for… The in statement deconstructs it.
fun main(args: Array<String>) {
var map = mutableMapOf<Int, String>()
map.put(10."Bill")
map.put(20."Jack")
map.put(30."Mike")
for ((key, value) in map) {
println(" key = [ $key ] , value = [ $value]. "")}}Copy the code
Output result:
key = [ 10 ] , value = [ Bill ]
key = [ 20 ] , value = [ Jack ]
key = [ 30 ] , value = [ Mike ]
A collection of
Although Kotlin can use the collections provided by the JDK, the Kotlin standard library still provides its own collections API. Unlike Java, the Kotlin standard library divides collections into modifiable and non-modifiable. In Kotlin, non-modifiable collection apis include List, Set, Map, and so on; The Mutable collection API includes MutableList, MutableSet, and MutableMap. These apis are all interfaces, and they are all subinterfaces of the Collection.
Kotlin designed it this way to avoid bugs as much as possible. Because collection variables are determined to be read-only or read-write at the time they are declared, you can avoid accidentally writing data to the collection during operations. Since Byte Code does not have instructions for read-only collections, Kotlin implements read-only collections using syntactic sugar. Many readers may remember the out keyword used to declare generics. If a generic is declared out, it can only be used for read operations. Therefore, the previously mentioned immutable collection apis (List, Set, Map) all declare generics using out when they are defined.
/ / the List interface
public interface List<out E> : Collection<E> {... }/ / Set the interface
public interface Set<out E> : Collection<E> {... }/ / the Map interface
public interface Map<K, out V> {... }Copy the code
Obviously, all three pieces of code use out declaration generics, where Map only uses out to declare V and K represents the Map key.
Here are some common ways to set.
val numbers:MutableList<Int> = mutableListOf<Int> (1.2.3.4) // Create a list object that can be read and written
val readOnlyViews:List<Int> = numbers // Make the read-write list read-only
println(numbers) // output: [1,2,3]
numbers.add(4) // Add a new element to numbers
println(readOnlyViews) // output result :[1,2,3,4]
readOnlyViews.clear() // Compile error, no clear() function
val strings = hashSetOf("a"."b".'c'.'d')
println(strings) [a, b, c, d]
Copy the code
As you can see from this code, the collection class does not provide a constructor to create collection objects, but instead provides functions to create collection objects. Here are some common functions for creating collection objects.
listOf
: used to createList
Object.setOf
: used to createSet
Object.mapOf
: used to createMap
Object.mutableListOf
: used to createMutableList
Object.mutableSetOf
: used to createMutableSet
Object.mutableMapOf
: used to createMutableMap
Object.
The following are examples of some of the above functions.
// Create a List object
var items = listOf(1.2.3.4.5)
// Create a MutableList object
var mutableList = mutableListOf(4.5.6)
// Create a Map object
var map = mapOf<String, Int>(Pair ("Jack".20), Pair("Mike".30))
Copy the code
When generics are defined, List
is considered a subtype of List
if out is used. According to this rule, List, Set, and Map all comply with this rule, while MutableList, MutableSet, and MutableMap do not comply with this rule because they do not use out to define generics.
For a read-write collection, you can convert it to a read-only version using the toXxx function, where Xxx is List, Set, and Map.
Range of values
Value range application
The value range expression is implemented using the rangeTo function, which takes the form of two points (..) , and two related operators, in and! The in. Any comparably sized data type can define a value range, but for integer primitive types, the implementation of the value range is specially optimized. The following is an example.
var n = 20
if (n in 1.100.) { If (n >= 1 &&n <= 10)
println("Meet the requirements")}if (n !in 30.80.) { / / equivalent to Java code if (n < 30 | | n > 80).
println("Not meeting the requirements")}Copy the code
Integer value ranges have an additional function of traversing them. The compiler takes care to transform this code into Java’s subscription-based for loop without incurring unnecessary performance costs.
for (i in 1.10.) { // for (int I = l; i <= 10; i++)
println("$i*$i = ${i * i}")}Copy the code
Output result:
1 times 1 is 1
2 times 2 is 4
3 times 3 is 9
4 times 4 is 16
5 times 5 is 25
6 times 6 is 36
7 times 7 is 49
8 times 8 is 64
9 times 9 is 81
10 times 10 is 100
What if I want to output in reverse order? You need to use the downTo function from the standard library.
for (i in 10 downTo 1) {
println("$i*$i = ${i * i}")}Copy the code
In the previous code, I was incremented or subtracted sequentially, with a step size of 1. If you want to change the step size, use the step function.
for (i in 1.10. step 2) { // Change the step size in sequence
println("$i*$i = ${i * i}")}for (i in 10 downTo 1 step 3) { // Change the step size in reverse order
println("$i*$i = ${i * i}")}Copy the code
In the previous code, the ranges used were closed intervals. For example, 1.. 10 means 1 <= I <= 10. To express 1 <= I < 10, use the unitil function as follows:
for (i in 1 until 10) {// I in [1,10], not including 10
println("$i*$i = ${i * i}")}Copy the code
Output result:
1 times 1 is 1
2 times 2 is 4
3 times 3 is 9
4 times 4 is 16
5 times 5 is 25
6 times 6 is 36
7 times 7 is 49
8 times 8 is 64
9 times 9 is 81
How value ranges work
The value range implements a common interface ClosedRange
in the standard library. ClosedRange
represents a mathematical closed interval consisting of comparable types. This interval includes two endpoints: start and endlnclusive, both of which are included in the value range. The main operation is contains, mainly through in/! Call as the in operator.
Progression of integer types (IntProgression, LongProgression, and CharProgression) represents an integer progression in arithmetic. The sequence is defined by a first element, a last element, and a non-zero increment. The first element is first, and all the subsequent elements are equal to the previous element plus increment. Unless the number column is empty, the last element must be reached when iterating through the number column.
Columns are subtypes of Iterable
, where N stands for Int, Long, Char, and so on. Columns can be used in for loops, maps, filters, and so on. Traversal in Progression is equivalent to the subscription-based for loop in Java/JavaScript.
for (inti = first; i ! = last; i += increment) { ... }Copy the code
For integer types, the range operator (..) Create an object that implements The ClosedRange
and *Progression interfaces. For example, IntRange implements ClosedRange
and inherits the IntProgression class, so all actions defined on IntProgression are valid for IntRange. So the result of downTo and step is always a *Progression.
To construct a sequence, use the fromClosedRange function defined in the companion object of the corresponding class.
IntProgression.romClosedRange(rangeStart: Int, rangeEnd: Int, step: Int)
Copy the code
Common utility functions
- **rangeTo: ** defined on integer types
rangeTo
Operator that is simply called*Range
Class constructor. Floating point value(Double and Float)
They don’t define their ownrangeTo
Instead, use the standard library for commonComparable
The operator provided by the. - DownTo: reversely iterates numbers
- Reversed: Returns the opposite sequence of numbers
- Step: Change the step size
Type checking and conversion
Is with the! Is the operator
We can use the IS operator to check at run time whether the object is consistent with a given type, or use the opposite! Is operator.
var obj: Any = 456
if (obj is String) { // Check whether obj is a String
println("Obj is a string")}if (obj is Int) { // check whether obj is an Int
println("Obj is an Int")}if (obj !is Int) {
println("Obj is not an Int")}Obj is an Int
Copy the code
Intelligent type conversion
In many cases, you don’t have to use explicit conversions in Kotlin because the compiler tracks is checks for immutable values and then automatically inserts (safe) conversions when needed.
fun demo(x: Any) {
if (x is String) {
print("x = $x , x.length = ${x.length}") // x is automatically converted to a string}}fun main(args: Array<String>) {
var str="hello"
demo(str)
}
// Output: x = hello, x.length = 5
Copy the code
If an opposing type check results in a return, the compiler is smart enough to determine that the conversion is safe.
if (x !is String) return
print(x.length) // x is automatically converted to a string
Copy the code
The right side of the or in && and | | :
/ / ` | | x to the right of the ` automatically converted to a string
if (x !is String || x.length == 0) return
// the x to the right of '&&' is automatically converted to a string
if (x is String && x.length > 0) {
print(x.length) // x is automatically converted to a string
}
Copy the code
This smart type conversion works equally well for when expressions and while loops.
var x:Any = "abc"
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
Copy the code
Note that smart conversions cannot be used when the compiler cannot guarantee that a variable is immutable between detection and use. More specifically, smart transformations can be applied according to the following rules:
- valLocal variables — always, except for local delegate properties;
- valProperty – If the property is private or internal, or the detection is performed in the same module where the property is declared. Smart conversions do not apply to open properties or properties with custom getters;
- varLocal variables — if the variable is not modified between detection and use, is not captured in the lambda that would modify it, and is not a local delegate attribute;
- varAttribute – never (because this variable can be modified by other code at any time).
Forced type conversion
If the type is cast and the types are incompatible, the cast operator will usually throw an exception. Therefore, we call it unsafe. In Kotlin, unsafe type conversions use the infix operator as.
val x = "hello"
val y: Int = x as Int
/ / compile error: the Exception in the thread "is the main" Java. Lang. ClassCastException: Java. Lang. String always be cast to Java. Lang. Integer
Copy the code
Note that null cannot be converted to String because the type is not nullable, that is, if x is null, the above code will throw an exception. To use such code for nullable values, use nullable types on the right side of the type conversion:
val x: Any? = "hello"
val y: Int? = x as Int?
Copy the code
To avoid throwing exceptions, we can use the safe conversion operator “as?” When the conversion fails, it returns NULL.
val x: Any? = "hello"
val y: Int? = x as? Int
println(y) // Output result: null
Copy the code
Note that despite the fact that as? To the right of is a non-null Int, but the result of its conversion is nullable.
This expression
To indicate the current recipient we use this expression:
- Among the members of the class,thisRefers to the current object of the class.
- In extension functions or function literals with receivers,thisRepresents the passed to the left of the pointThe receiverParameters.
If this has no qualifier, it refers to the innermost scope containing it. To refer to this in another scope, use the label qualifier.
To access an outer scope (such as this inside a class, extension function, or labeled receiver function literal >), we use this@label, where @label is a label that represents the scope to which we want to access this.
class A { // Implicit tag @a
inner class B { // Implicit tag @b
fun Int.foo(a) { // The implicit tag @foo
val a = this@A / / A of this
val b = this@B / / B of this
val c = this // The receiver of foo(), an Int
val c1 = this@foo // The receiver of foo(), an Int
val funLit = lambda@ fun String.(a) {
val d = this // The recipient of funLit
}
val funLit2 = { s: String ->
// the recipient of foo() because it contains the lambda expression
// There is no receiver
val d1 = this}}}}Copy the code
equality
There are two types of equality in Kotlin:
- Be equal in construction
equals()
Detection); - References are equal (two references refer to the same object).
Referential equality is defined by === (and its negative form! ==) operation judgment. A === b Evaluates to true if and only if a and b refer to the same object. For values (such as Int) that are represented at runtime as native types, the === equality detection is equivalent to the == detection.
Structural equality is determined by == (and its negative form! =) operation judgment. By convention, expressions like a == b translate to:
a? .equals(b) ? : (b ===null)
Copy the code
Equals (Any?) is called if a is not null. Function, otherwise (that is, if a is null) checks whether B is equal to the null reference.
Operator overloading
Kotlin allows us to provide implementation functions for a predefined set of operators for data types. These operators have a fixed notation (such as + or *) and a fixed order of precedence. To implement these operators, we need to implement a fixed-name member function or extension function for the corresponding data type, which for binary operators is the type of the left-hand operand and for unary operators is the type of the unique operand. Functions used to implement operator overloading should be marked with the operator modifier.
Unary operator overloading
The unary operator is related to the corresponding function as follows:
expression | Corresponding function |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
! a | a.not() |
a++ | a.inc() |
a– | a.dec() |
When the compiler processes, for example, an expression +a, it performs the following steps:
- determine
a
Of the type, supposeT
; - For the receiver
T
Find a file withoperator
Modifier is a parameterless functionUnaryPlus ()
, that is, member functions or extension functions; - If the function does not exist or is ambiguous, a compilation error will result;
- If the function exists and its return type is
R
That’s the expression+a
Have a typeR
;
Note that these and all other operations are optimized for primitive types and do not introduce function call overhead for them.
Here is an example of how to overload unary operators:
data class Point(val x: Int.val y: Int)
operator fun Point.unaryMinus(a) = Point(-x, -y)
val point = Point(10.20)
fun main(a) {
println(-point) // Point(x=-10, y=-20)
}
Copy the code
A.inic and a.dac in the table should change their recipients and return a value (optional). Inc ()/dec() should not change the value of the receiver object. By “changing their receiver”, we mean changing the receiver variable, not the value of the receiver object.
For postfix operators, such as a++, the compiler performs the following steps when parsing.
- determine
a
The type of theT
; - Find an application for a type of
T
The receiver ofoperator
Modifier is a parameterless functioninc()
; - The return type of the detection function is
T
Subtype of.
The steps to evaluate the expression are:
- the
a
The initial value is stored to temporary storagea0
; - the
a0.inc()
The result is assigned toa
; - the
a0
Return as the result of an expression.
For a–, the steps are exactly similar.
For the prefix form ++a and –a to be resolved in the same way, the steps are:
- the
a.inc()
The result is assigned toa
; - the
a
The new value of is returned as the expression result.
The binary operation
Arithmetic operator
expression | Corresponding function |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
a % b |
a.rem(b) ,a.mod(b) (Deprecated) |
a.. b |
a.rangeTo(b) |
For operations in this table, the compiler simply parses them into expressions translated into columns.
Note that rem operators have been supported since Kotlin 1.1. Kotlin 1.0 uses the mod operator, which was deprecated in Kotlin 1.1.
Here is an example of a Counter class starting from a given value, which can increment the count using the overloaded + operator:
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
Copy the code
“In” operator
expression | Corresponding function |
---|---|
a in b |
b.contains(a) |
a ! in b |
! b.contains(a) |
For in and! In, the process is the same, but the order of the arguments is reversed.
Index access operator
expression | Corresponding function |
---|---|
a[i] |
a.get(i) |
a[i, j] |
a.get(i, j) |
A [i_1,......, i_n] |
Atul gawande et (i_1,... , i_n) |
a[i] = b |
a.set(i, b) |
a[i, j] = b |
a.set(i, j, b) |
A [i_1,...... i_n] = b |
A.s et (i_1,... , i_n, b) |
Square brackets translate into calls to GET and set with the appropriate number of arguments.
Call operator
expression | Corresponding function |
---|---|
a() |
a.invoke() |
a(i) |
a.invoke(i) |
a(i, j) |
a.invoke(i, j) |
A (i_1,... , i_n) |
Anderson nvoke (i_1,... , i_n) |
The parentheses are converted to invoke with the appropriate number of parameters.
The generalized assignment
expression | Corresponding function |
---|---|
a += b |
a.plusAssign(b) |
a -= b |
a.minusAssign(b) |
a *= b |
a.timesAssign(b) |
a /= b |
a.divAssign(b) |
a %= b |
a.remAssign(b) .a.modAssign(b) (Deprecated) |
For assignment operations, such as a += b, the compiler performs the following steps:
- If the function in the right column is available
- If the corresponding function of two variables (i.e
plusAssign()
Corresponding to theplus()
) is also available, then report an error (vague), - Make sure its return type is
Unit
, or report an error, - generate
a.plusAssign(b)
Code;
- If the corresponding function of two variables (i.e
- Otherwise try to generate
a = a + b
Type detection is included here:a + b
Must be of typea
Subtype of.
Note: Assignment is not an expression in Kotlin.
The equality and inequality operators
expression | Corresponding function |
---|---|
a == b |
a? .equals(b) ? : (b === null) |
a ! = b |
! (a? .equals(b) ? : (b === null)) |
These operators only use equals(other: Any?). : Boolean, which can be overridden to provide a custom equality detection implementation. No other functions of the same name (such as equals(other: Foo)) are called.
Note: === and! == (identity detection) is not overloaded, so there is no convention for them.
The == operator is somewhat special: it is translated into a complex expression that filters for null values. Null == null is always true, and for non-empty x, x == null is always false without calling x.equals().
Comparison operator
expression | Corresponding function |
---|---|
a > b |
a.compareTo(b) > 0 |
a < b |
a.compareTo(b) < 0 |
a >= b |
a.compareTo(b) >= 0 |
a <= b |
a.compareTo(b) <= 0 |
All comparisons are converted to calls to compareTo, which need to return an Int
Air safety
Nullable and non-nullable types
Kotlin’s type system is designed to eliminate the danger of empty references from code, also known as the billion Dollar Mistake.
One of the most common pitfalls in many programming languages, including Java, is that accessing a member of an empty reference causes a null-reference exception. In Java, this is equivalent to a NullPointerException or NPE for short.
Kotlin’s type system is designed to eliminate NullPointerExceptions from our code. The only possible cause of NPE may be:
- Explicitly call
throw NullPointerException()
; - The ones described below are used
!!!!!
The operator; - Some data is inconsistent when initialized, for example when:
- Pass an uninitialized object that appears in the constructorthisAnd used elsewhere (” Leakagethis“);
- The constructor of the superclass calls an open member whose uninitialized state is used by the implementation of the derived class;
- Java interoperability:
- Attempting to access the platform type
null
Referenced members; - Generic types used for Java interoperability with nullability errors, such as a piece of Java code that might be directed to Kotlin’s
MutableList<String>
addnull
, which means it should be usedMutableList<String? >
To deal with it; - Other problems caused by external Java code.
- Attempting to access the platform type
In Kotlin, the type system distinguishes between a reference that can hold NULL (nullable references) and one that cannot (non-nullable references). For example, a regular variable of type String cannot hold NULL:
var a: String = null // compile error, a cannot be null
var b: String = "abc" // By default, normal initialization means non-null
b = null // Error compiling
Copy the code
If we want to allow nullability, we can declare a variable as a nullable String, String, okay? :
var a: String = "xyz"
var b: String? = "abc" // Can be set to null
b = null // ok
print(b)
Copy the code
Now, if you call a’s methods or access its properties, it is guaranteed not to cause an NPE, so you can safely use:
val l = a.length
Copy the code
But if you want to access the same property of B, then this is not secure, and the compiler reports an error:
val l = b.length // Error: variable "b" may be empty
Copy the code
But we still need access to that property, right? There are several ways to do this.
Test in conditionsnull
First, you can explicitly detect whether b is null and handle two possibilities:
val l = if(b ! =null) b.length else -1
Copy the code
The compiler keeps track of the information being checked and allows you to call length inside if. More complex (smarter) conditions are also supported:
val b: String? = "Kotlin"
if(b ! =null && b.length > 0) {
print("String of length ${b.length}")}else {
print("Empty string")}Copy the code
Note that this only applies if B is immutable (that is, a local variable that has not been modified between detection and use, or a val member that cannot be overridden and has a background field), because otherwise it could happen that b becomes null again after detection.
Secure call
Your second option is to safely call the operator, write? . :
val a = "Kotlin"
val b: String? = nullprintln(b? .length) println(a? .length)// No security calls required
Copy the code
If b is not empty, return b.length, otherwise return null, this expression is of type Int? .
Security calls are useful in chained calls. For example, if an employee Bob may (or may not) be assigned to a department, and there may be another employee who is the head of that department, then get the name of the head (if any) of Bob’s department, and we write:
bob? .department?.head?.nameCopy the code
If any of the attributes (segments) are empty, the chain call returns NULL.
If you want to perform an operation only on non-null values, the safe call operator can be used with let:
vallistWithNulls: List<String? > = listOf("Kotlin".null)
for (item inlistWithNulls) { item? .let { println(it) }// Print Kotlin and ignore null
}
Copy the code
Security calls can also appear on the left side of an assignment. Thus, if any receiver in the call chain is empty the assignment is skipped, and the expression on the right is not evaluated at all:
// If either 'person' or 'person.department' is empty, this function will not be called:person? .department?.head = managersPool.getManager()Copy the code
Elvis operator
When we have a nullable reference to b, we can say “if B is not empty, I use it; Otherwise use some non-empty value “:
val l: Int = if(b ! =null) b.length else -1
Copy the code
In addition to the full if-expression, this can also be expressed via the Elvis operator, write? : :
val l = b? .length ? 1: -Copy the code
If? The Elvis operator returns the left-hand expression if it is not empty, or the right-hand expression if it is not. Note that the right-hand side of the expression is evaluated if and only if the left-hand side is empty.
Note that because throw and return are expressions in Kotlin, they can also be used on the right side of the Elvis operator. This can be very handy, for example, to detect function arguments:
fun foo(node: Node): String? {
valparent = node.getParent() ? :return null
valname = node.getName() ? :throw IllegalArgumentException("name expected")
/ /...
}
Copy the code
!!!!!
The operator
The third option is for NPE enthusiasts: non-empty assertion operators (!!). Converts any value to a non-null type and throws an exception if the value is null. We could write b!! , which either returns a non-empty value of b (for example, String in our example) or raises an NPE exception if b is empty:
vall = b!! .lengthCopy the code
So if you want an NPE, you can get it, but you have to ask for it explicitly or it won’t come by surprise.
Safe type conversion
If the object is not of the target type, a normal cast may cause a ClassCastException. Another option is to use a safe conversion that returns NULL if the conversion attempt is unsuccessful:
val aInt: Int? = a as? Int
Copy the code
A collection of nullable types
If you have a collection of nullable elements and want to filter non-empty elements, you can do this using filterNotNull:
val nullableList: List<Int? > = listOf(1.2.null.4)
val intList: List<Int> = nullableList.filterNotNull()
Copy the code
abnormal
Exception class
All exception classes in Kotlin are subclasses of the Throwable class. Each exception has a message, stack traceback information, and an optional reason for the error.
Throw an exception using throw-expressions.
throw Exception("Hi There!")
Copy the code
Use try-expressions to catch exceptions:
try {
// Some code
}catch (e: SomeException) {
// Handler
}finally {
// Optional finally block
}
Copy the code
There can be zero to multiple catch blocks. The finally block can be omitted. But there should be at least one catch and finally block.
Try is an expression
Try is an expression that can have a return value:
val a: Int? = try {
parseInt(input)
} catch (e: NumberFormatException) {
null
}
Copy the code
The return value of a try- expression is either the last expression in a try block or (all) the last expression in a catch block. The contents of the finally block do not affect the result of the expression.
Abnormalities under examination
Kotlin showed no abnormalities. There are many reasons for this, but we will provide a simple example.
Here is an example interface implemented by the StringBuilder class in the JDK:
Appendable append(CharSequence csq) throws IOException;
Copy the code
What does this signature mean? It means that every time I append a string to something (a StringBuilder, some kind of log, a console, etc.) I have to catch those IOexceptions. Why is that? Because it may be performing IO operations (Writer implements Appendable as well)… So it leads to code like this appearing everywhere:
try {
log.append(message)
}
catch (IOException e) {
// It must be safe
}
Copy the code
The result is not good. Experiments in small applications have shown that requiring exception information in method definitions can improve developer productivity and code quality, but experience in large applications points to a different conclusion: productivity decreases and code quality improves little or no.
Nothing type
In Kotlin a throw is an expression, so you can use it (for example) as part of an Elvis expression:
val s = person.name ?: throw IllegalArgumentException("Name required")
Copy the code
The type of the throw expression is the special type Nothing. This type has no value and is used to mark code locations that can never be reached. In your own code, you can use Nothing to mark a function that never returns:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
Copy the code
When you call this function, the compiler knows to stop executing after the call:
val s = person.name ?: fail("Name required")
println(s) // it is known here that "s" is initialized
Copy the code
Another situation in which you might encounter this type is type inference. The nullable variant of this type Nothing? One possible value is null. If null is used to initialize a value of the inferred type, and no other information is available to determine the more specific type, the compiler will infer Nothing? Type:
val x = null // "x" has type 'Nothing? `
val l = listOf(null) // "l" has type 'List
?>
Copy the code
annotations
Annotation statement
Annotations are a way to attach metadata to code. To declare an annotation, place the Annotation modifier in front of the class:
annotation class Fancy
Copy the code
Additional attributes for annotations can be specified by annotating annotation classes with meta-annotations:
- @target specifies the possible types (classes, functions, attributes, expressions, and so on) of elements that can be annotated with the annotation;
- @Retention specifies whether the annotation is stored in the compiled class file and whether it is visible through reflection at runtime (both default to true);
- Repeatable allows the same annotation to be used multiple times on a single element;
- @Mustbedocumented Specifies that the annotation is part of the public API and should be included in the signature of the class or method displayed in the generated API documentation.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
Copy the code
usage
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)}}Copy the code
If you need to annotate the class’s main constructor, add the constructor keyword to the constructor declaration and add the annotation in front of it:
class Foo @Inject constructor(the dependency: MyDependency) {... }Copy the code
You can also annotate property accessors:
class Foo {
var x: MyDependency? = null
@Inject set
}
Copy the code
The constructor
Annotations can have constructors that accept arguments.
annotation class Special(val why: String)
@Special("example") class Foo {}
Copy the code
The allowed parameter types are:
- Types corresponding to Java native types (Int, Long, and so on);
- The string;
- Class (
Foo::class
); - Enumeration;
- Other notes;
- Array of the column type above.
Annotation parameters cannot have nullable types because the JVM does not support storing NULL as the value of an annotation attribute.
If the annotation is used as an argument to another annotation, its name is not prefixed with the @ character:
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))
Copy the code
If you need to specify a class as an argument to an annotation, use the Kotlin class (KClass). The Kotlin compiler automatically converts this to a Java class so that Java code can access the annotations and parameters normally.
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
Copy the code
Lambda expressions
Annotations can also be used with lambda expressions. They are applied to the invoke() method that generates the body of a lambda expression. This is useful for frameworks like Quasar, which use annotations for concurrency control.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
Copy the code
Annotations use the target
When an attribute or primary constructor parameter is annotated, more than one Java element is generated from the corresponding Kotlin element, so the annotation has multiple possible locations in the generated Java bytecode. To specify exactly how the annotation should be generated, use the following syntax:
@field:Ann val foo, @get:Ann val bar, // Annotate Java getter@param :Ann val quux) // Annotate Java constructor argumentsCopy the code
You can annotate the entire file using the same syntax. To do this, place the annotation with the target file at the top of the file, before the package directive, or before all imports (if the file is in the default package) :
@file:JvmName("Foo")
package org.jetbrains.demo
Copy the code
If you have multiple annotations for the same target, you can avoid target duplication by adding square brackets after the target and placing all annotations in square brackets:
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
Copy the code
The complete list of supported user targets is:
file
;property
(Annotations with this target are not visible to Java);field
;get
(property getter);set
(property setter);receiver
(extending the receiver parameters of a function or attribute);param
(constructor argument);setparam
(property setter parameters);delegate
Field that stores its delegate instance for the delegate property.
To annotate the receiver parameters of an extension function, use the following syntax:
fun @receiver:Fancy String.myExtension() { ... }
Copy the code
If no Target is specified, the Target is selected based on the @target annotation of the annotation being used. If there are more than one applicable target, use the first applicable target in the following list:
param
;property
;field
.
Java annotations
Java annotations are 100% Kotlin compatible:
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*
class Tests {
// Apply the @rule annotation to the property getter
@get:Rule val tempFolder = TemporaryFolder()
@Test fun simple(a) {
val f = tempFolder.newFile()
assertEquals(42, getTheAnswer())
}
}
Copy the code
Because java-written annotations do not define the order of arguments, you cannot use normal function call syntax to pass arguments. Instead, you need to use named parameter syntax:
// Java
public @interface Ann {
int intValue(a);
String stringValue(a);
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
Copy the code
As in Java, a special case is the value parameter; Its value need not be specified by an explicit name:
// Java
public @interface AnnWithValue {
String value(a);
}
// Kotlin
@AnnWithValue("abc") class C
Copy the code
Array as an annotation parameter
If the value argument in Java has an array type, it becomes a vararg argument in Kotlin:
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
Copy the code
For other arguments that have array types, you need to explicitly use array literals (since Kotlin 1.2) or arrayOf(…). :
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
/ / Kotlin + 1.2:
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C/ / the old versionKotlin: @AnnWithArrayMethod(names = arrayOf("abc"."foo"."bar"))
class D
Copy the code
Access attributes of the annotation instance
The value of the annotation instance is exposed to the Kotlin code as an attribute:
// Java
public @interface Ann {
int value(a);
}
Copy the code
// Kotlin
fun foo(ann: Ann) {
val i = ann.value
}
Copy the code
reflection
Reflection is a set of language and library features that allow you to introspect the structure of your program at run time. Kotlin makes functions and attributes in the language first-class citizens, and introspection about them (that is, knowing a name or the type of an attribute or function at run time) is closely related to simply using functional or reactive styles.
Class reference
The most basic reflection function is to get a runtime reference to the Kotlin class. To get a reference to a statically known Kotlin class, use the class literal syntax:
val c = MyClass::class
Copy the code
The reference is a value of type KClass.
Note that Kotlin class references are different from Java class references. To get a Java class reference, use the.java property on the KClass instance.
Enumeration class member
One of the most common functions of reflection is to enumerate the members of a class, such as class properties, methods, and so on. In Kotlin’s reference class, there are several memberXxx functions that do this. Where Xxx is Properties, Functions, etc. The following code uses the corresponding functions to get all the members of the Person class, as well as all the attributes and all the functions individually.
open class Person constructor(var name: String) { // declare the main constructor
var mName: String = "Bill" // Initializes member attributes
var age: Int = 0
var gender: Int = 0
init {
this.mName = name
println("name = [ $name]. "")}// declare the secondary constructor (this calls the primary constructor directly)
constructor(name: String, age: Int) : this(name) {
this.mName = name
this.age = age
}
// declare the secondary constructor (this calls the secondary constructor, indirectly calling the primary constructor)
constructor(name: String, age: Int, gender: Int) : this(name, age) {
this.gender = gender
println("name = [${name}], age = [${age}], gender = [${gender}]. "")}/** * kotlin function default **@paramThe name name *@paramThe age age *@paramGender gender * /
fun setPersonInfo(name: String = "bill", age: Int = 23, gender: Int) {
println("name = [${name}], age = [${age}], gender = [${gender}]. "")
this.mName = name
this.age = age
this.gender = gender
}
override fun toString(a): String {
return "Person(name='$mName', age=$age, gender=$gender)"}}// Get the class application of the Person class
val c = Person::class
fun main(args: Array<String>) {
// Get the list of all members of the Person class (attributes and functions)
println("The number of members of the Person class:${c.members.size}")
// Enumerate all members of the Person class
println("============= enumerate all members of the Person class =============")
for (member in c.members) {
// Prints the name and type of each member
println("Name of member:${member.name}, return type:${member.returnType}")}// Get the number of all attributes in Person
println("Number of attributes:${c.memberProperties.size} ")
// Enumerate all functions in the Person class
println("============= enumerate all functions in the Person class =============")
for (function in c.functions) {
// Outputs the function name and return type
println("Function name:${function.name}, return type:${function.returnType}")}// Enumerate all attributes in the Person class
println("============= enumerate all attributes in the Person class =============")
for (property in c.memberProperties) {
// Output the attribute name and return type
println("Attribute name:${property.name}, return type:${property.returnType}")}}Copy the code
Output result:
Call member functions dynamically
Another important use of reflection is the ability to call members of an object dynamically, such as member functions, member properties, and so on. The so-called dynamic call is called according to the name of the class member, which can be dynamically specified.
Members of a class can be returned directly with the :: operator. For example, MyClass has a process function. MyClass:: Process fetches the object of that member function, and then invokes the process function.
However, this does not specify the name of the process function dynamically, but rather hardcodes the function name into the code. However, by invoking Java’s reflection mechanism, you can dynamically specify a function name and call that function.
fun main(args: Array<String>) {
// Get the setPersonInfo function object
var p = Person::setPersonInfo
var person = Person("jack")
// Invoke the setPersonInfo function
p.invoke(person, "mike".23.1)
// Use Java reflection to specify the name of the setPersonInfo method
var method = Person::class.java.getMethod(
"setPersonInfo",
String::class.java,
Int: :class.java,
Int: :class.java
)
method.invoke(person, "Bill".25.0)}// Output result:
// name = [ jack ]
// name = [mike], age = [23], gender = [1]
// name = [Bill], age = [25], gender = [0]
Copy the code
Invoke member properties dynamically
Properties of the Kotlin class, like functions, can be called dynamically using reflection. However, when the Kotlin compiler processes Kotlin class properties, it converts them into getter and setter methods, rather than Java fields with the same name as the property. For example, for the Person class below, the name property becomes two methods, getName and setName, after compilation.
fun main(args: Array<String>) {
var person = Person("jack")
var name = person::name
println(name.get()) // Output result :jack
name.set("Tom")
println(name.get()) // Output result :Tom
// Get the getName method using Java reflection
var getName = Person::class.java.getMethod("getName")
// Dynamically get the value of the name attribute
println("Java reflected name =" + getName.invoke(person)) // Output: Java reflected name = Tom
}
Copy the code
Scope function
The Kotlin library contains several functions whose sole purpose is to execute a block of code in the context of an object. When such a function is called on an object and a lambda expression is provided, it forms a temporary scope. In this scope, the object can be accessed without its name. These functions are called scoped functions. There are five types: let, run, with, apply and also.
These functions basically do the same thing: execute a block of code on an object. The difference is how this object is used in the block and what the result of the entire expression is.
Here is a typical use of a scope function:
Person("Alice".20."Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
Copy the code
If you didn’t write this code using let, you would have to introduce a new variable and repeat its name each time you used it.
val alice = Person("Alice".20."Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
Copy the code
Scoped functions don’t introduce any new techniques, but they can make your code cleaner and more readable.
Choosing the right function for your case can be a bit tricky due to the similar nature of scoped functions. The choice depends largely on your intentions and consistency of use in the project. Below we describe in detail the differences between the various scoped functions and their convention usage.
The difference between
Because scoped functions are all very similar in nature, it is important to understand the differences between them. There are two main differences between each scope function:
- The way context objects are referenced
- The return value
Context object:this
orit
In lambda expressions of scoped functions, context objects can be accessed using a shorter reference rather than their actual name. Each scoped function accesses the context object in one of two ways: as the receiver of a lambda expression (this) or as the argument to a lambda expression (it). Both offer the same functionality, so we’ll describe the pros and cons of both for different scenarios and provide recommendations for their use.
fun main(a) {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver String length: ${this.length}") // Same effect as The last sentence
}
// it
str.let {
println("The receiver string's length is ${it.length}")}}Copy the code
this
Run, with, and apply refer to context objects through the keyword this. Therefore, context objects can be accessed in their lambda expressions just as they would in normal class functions. In most scenarios, you can omit this when accessing the receiver object to make your code shorter. By contrast, if this is omitted, it becomes difficult to distinguish between members of the recipient object and external objects or functions. Therefore, for lambda expressions that operate primarily on object members (calling their functions or assigning their attributes), it is recommended that the context object be the receiver (this).
val adam = Person("Adam").apply {
age = 20 // This. Age = 20 or adam.age = 20
city = "London"
}
println(adam)
Copy the code
it
In turn, let and also take context objects as lambda expression arguments. If no parameter name is specified, the object can be accessed with the implicit default name it. It is shorter than this, and expressions with it are usually easier to read. However, when calling object functions or properties, you cannot access objects implicitly like this. Therefore, it is better to use it as the context object when the context object is primarily used as a parameter in a function call in scope. It is also better if you use multiple variables in a code block.
fun getRandomInt(a): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")}}val i = getRandomInt()
Copy the code
In addition, when a context object is passed as a parameter, a custom name in scope can be specified for the context object.
fun getRandomInt(a): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")}}val i = getRandomInt()
Copy the code
The return value
Based on the result returned, scope functions can be classified into the following two categories:
apply
及also
Returns a context object.let
,run
及with
Returns the result of the lambda expression.
These two options allow you to select the appropriate function based on subsequent actions in the code.
Context object
The return value of apply and also is the context object itself. Therefore, they can be included in the call chain as auxiliary steps: you can continue to make chained function calls on the same object.
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
Copy the code
They can also be used in return statements of functions that return context objects.
fun getRandomInt(a): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")}}val i = getRandomInt()
Copy the code
Lambda expression result
Let, run, and with return the result of a lambda expression. So you can use them when you need to assign a value to a variable using their results, or when you need to chain the results, etc.
val numbers = mutableListOf("one"."two"."three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
Copy the code
Alternatively, you can create a temporary scope for a variable using only the scope function, ignoring the return value.
val numbers = mutableListOf("one"."two"."three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")}Copy the code
Several functions
To help you choose the right scope functions for your scenario, we describe them in detail and provide some suggestions for their use. Technically, scoped functions are interchangeable in many scenarios, so these examples show the use of conventions that define a common usage style.
let
The context object is accessed as an argument (it) to a lambda expression. The return value is the result of a lambda expression.
Let can be used to call one or more functions on the result of the call chain. For example, the following code prints the results of two operations on a collection:
val numbers = mutableListOf("one"."two"."three"."four"."five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
Copy the code
Using let, we can write it like this:
val numbers = mutableListOf("one"."two"."three"."four"."five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// More functions can be called if necessary
}
Copy the code
If the code block contains only a single function that takes it as an argument, a method reference (::) can be used instead of a lambda expression:
val numbers = mutableListOf("one"."two"."three"."four"."five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
Copy the code
Let is often used to execute a block of code with only non-null values. If you need to operate on a non-empty object, you can use the safe call operator on it. . And calls let to perform operations in lambda expressions.
val str: String? = "Hello"
//processNonNullString(STR) // Compilation error: STR may be empty
vallength = str? .let { println("let() called on $it")
processNonNullString(it) // Compile through: 'it' in '? .let {}' must not be empty
it.length
}
Copy the code
Another way to use lets is to introduce scoped local variables to improve code readability. If you need to define a new variable for a context object, you can provide its name as a lambda expression parameter instead of the default IT.
val numbers = listOf("one", "two", "three", "four") val modifiedFirstItem = numbers.first().let { firstItem -> println("The first item of the list is '$firstItem'") if (firstItem.length >= 5) firstItem else "!" + firstItem + "!" }.toUpperCase() println("First item after modifications: '$modifiedFirstItem'")Copy the code
Target platform: JVMRunning on kotlin v. 1.6.0
with
A non-extension function: The context object is passed as an argument, but inside a lambda expression, it can be used as a receiver (this). The return value is the result of a lambda expression.
We recommend using with to call functions on context objects rather than lambda expression results. In code, with can be read as “For this object, do the following.”
val numbers = mutableListOf("one"."two"."three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")}Copy the code
Another use scenario for with is to introduce a helper object whose properties or functions will be used to evaluate a value.
val numbers = mutableListOf("one"."two"."three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
Copy the code
run
The context object is accessed as the receiver (this). The return value is the result of a lambda expression.
Run and with do the same thing, but are called in the same way as let — as extension functions for context objects.
Run is useful when lambda expressions include both object initialization and evaluation of return values.
val service = MultiportService("https://example.kotlinlang.org".80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")}// Use let() to write the same code:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")}Copy the code
In addition to calling run on the receiver object, it can also be used as a non-extension function. A non-extended run lets you execute a block of statements where an expression is needed.
val hexNumberRegex = run {
val digits = "0-9." "
val hexDigits = "A-Fa-f"
val sign = "+ -"
Regex("[$sign]? [$digits$hexDigits] +")}for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}
Copy the code
apply
The context object is accessed as the receiver (this). The return value is the context object itself.
Use Apply for blocks of code that return no value and run primarily on members of the receiver (this) object. A common case for Apply is object configuration. Such a call can be understood as “apply the following assignment operations to the object.”
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
Copy the code
With the receiver as the return value, you can easily include Apply in the call chain for more complex processing.
also
The context object is accessed as an argument (it) to a lambda expression. The return value is the context object itself.
Also is useful for performing operations that take context objects as arguments. Use also for operations that need to refer to an object rather than its properties and functions, or when you don’t want to mask this references from an external scope.
When you see also in code, you can think of it as “and do the following with that object.”
val numbers = mutableListOf("one"."two"."three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
Copy the code
Function to choose
To help you choose the right scope function, we have provided a table of the main differences between them.
function | Object reference | The return value | Extension function |
---|---|---|---|
let |
it |
Lambda expression result | is |
run |
this |
Lambda expression result | is |
run |
– | Lambda expression result | No: Call context-free objects |
with |
this |
Lambda expression result | Not: take the context object as an argument |
apply |
this |
Context object | is |
also |
it |
Context object | is |
Here is a short guide to selecting scope functions for their intended purpose:
- Executes a lambda expression on a non-null object:
let
- To introduce an expression as a variable into a local scope:
let
- Object configuration:
apply
- Object configuration and calculation results:
run
- Run statements where expressions are needed: non-extended
run
- Additional effects:
also
- A set of function calls to an object:
with
There are overlapping scenarios for the use of different functions, and you can choose the functions based on the specific conventions used on your project or team.
While scoped functions are a way to make your code more concise, avoid overusing them: they can make your code less readable and can lead to errors. Avoid nesting scoped functions, and be careful when calling them chained: it’s easy to get confused about the current context object and the value of this or it.
takeIf
与 takeUnless
In addition to the scoped functions, the library also contains the takeIf and takeUnless functions. These functions allow you to embed object state checks in the call chain.
When called on an object with the provided predicate, takeIf returns the object if it matches the predicate. Otherwise null is returned. Therefore, takeIf is a filter function for a single object. TakeUnless, on the other hand, returns an object if it does not match the predicate and null if it does. This object is accessed as a lambda expression parameter (it).
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2= =0 }
val oddOrNull = number.takeUnless { it % 2= =0 }
println("even: $evenOrNull, odd: $oddOrNull")
Copy the code
When chain-calling other functions after takeIf and takeUnless, don’t forget to perform null checks or security calls (? .). Because their return value is nullable.
val str = "Hello"
valcaps = str.takeIf { it.isNotEmpty() }? .toUpperCase()//val caps = str.takeIf {it.isnotempty ()}.toupperCase () // compilation error
println(caps)
Copy the code
TakeIf and takeUnless are particularly useful together with scoped functions. A good example is to link them with a let to run a code block on an object that matches a given predicate. To do this, call takeIf on the object and then use a security call (? .). Calls to let. For objects that do not match the predicate, takeIf returns NULL and does not call let.
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0}? .let { println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011"."11")
displaySubstringPosition("010000011"."12")
Copy the code
Without the library function, the same function looks like this:
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011"."11")
displaySubstringPosition("010000011"."12")
Copy the code