Kotlin From Scratch: Packages and Basic Functions
2022-4-30 13:43:0 Author: code.tutsplus.com(查看原文) 阅读量:19 收藏

Kotlin is a modern programming language that compiles to Java bytecode. It is free and open source, and promises to make coding for Android even more fun. 

In the previous article, you learned about ranges and collections in Kotlin. In this tutorial, we'll continue to learn the language by looking at how to organize code using packages, and then go on to an introduction to functions in Kotlin.

1. Packages

If you are familiar with Java, you know that Java uses packages to group related classes; for example, the java.util package has a number of useful utility classes. Packages are declared with the package keyword, and any Kotlin file with a package declaration at the beginning can contain declarations of classes, functions, or interfaces.

Declaration

Looking at the code below, we have declared a package com.chikekotlin.projectx using the package keyword. Also, we declared a class MyClass (we'll discuss classes in Kotlin in future posts) inside this package.

package com.chikekotlin.projectx

class MyClass

Now, the fully qualified name for the class MyClass is com.chikekotlin.projectx.MyClass.

package com.chikekotlin.projectx

fun saySomething(): String {
    return "How far?"
}

In the code above, we created a top-level function (we'll get to that shortly). So similarly to MyClass, the fully qualified name for the function saySomething() is com.chikekotlin.projectx.saySomething.

Imports

In Kotlin, we use the import declaration to enable the compiler to locate the classes, functions, interfaces or objects to be imported. In Java, on the other hand, we can't directly import functions or methods—only classes or interfaces. 

We use import to access a function, interface, class or object outside the package where it was declared. 

import com.chikekotlin.projectx.saySomething

fun main(args: Array<String>) {
    saySomething() // will print "How far?"
}

In the code snippet above, we imported the function saySomething() from a different package, and then we executed that function.

Kotlin also supports wildcard imports using the * operator. This will import all the classes, interfaces and functions declared in the package all at once. This is not recommended, though—it's usually better to make your imports explicit.

import com.chikekotlin.projectx.*

Import Aliasing

When you have libraries that have conflicting class or function names (e.g. they each declare a function with the same name), you can use the as keyword to give that imported entity a temporary name.

import com.chikekotlin.projectx.saySomething
import com.chikekotlin.projecty.saySomething as projectYSaySomething

fun main(args: Array<String>) {
    projectYSaySomething() 
}

Note that the temporary name is used only within the file where it was assigned.

2. Functions

A function groups together a series of code statements that perform a task. The details of the implementation of the function are hidden from the caller.

In Kotlin, functions are defined using the fun keyword, as shown in the following example:

fun hello(name: String): String {
    return "Hello $name"
}
val message = hello("Chike")
print(message) // will print "Hello Chike"

In the code above, we defined a simple function hello() with a single parameter name of type String. This function returns a String type. The parameter definition format for functions is name: type, e.g. age: Int, price: Double, student: StudentClass.

fun hello(name: String): Unit {
   print("Hello $name")
}

hello("Chike") // will print "Hello Chike"

The function above is similar to the previous one, but notice that this one has a return type of Unit. Because this function doesn't return any significant value to us—it just prints out a message—its return type is Unit by default. Unit is a Kotlin object (we'll discuss Kotlin objects in later posts) that is similar to the Void types in Java and C.

public object Unit {
    override fun toString() = "kotlin.Unit"
}

Note that if you don't explicitly declare the return type to be Unit, the type is inferred by the compiler.

fun hello(name: String) { // will still compile
   print("Hello $name")
}

Single-Line Functions

Single-line or one-line functions are functions that are just single expressions. In this function, we get rid of the braces and use the = symbol before the expression. In other words, we get rid of the function block.

fun calCircumference(radius: Double): Double {
    return (2 * Math.PI) * radius
}

The function above can be shortened into a single line:

fun calCircumference(radius: Double) = (2 * Math.PI) * radius

Looking at the updated function above, you can see that we have made our code more concise by removing the curly braces {}, the return keyword, and also the return type (which is inferred by the compiler). 

You can still include the return type to be more explicit if you want.

fun calCircumference(radius: Double): Double = (2 * Math.PI) * radius

Named Parameters

Named parameters allow more readable functions by naming the parameters that are being passed to a function when called.

In the following example, we created a function that prints my full name.

fun sayMyFullName(firstName: String, lastName: String, middleName: String): Unit {
    print("My full name is $firstName $middleName $lastName");
}

To execute the function above, we would just call it like so:

sayMyFullName("Chike", "Nnamdi", "Mgbemena")

Looking at the function call above, we don't know which String type arguments equate to which function parameters (though some IDEs such as IntelliJ IDEA can help us). Users of the function will have to look into the function signature (or the source code) or documentation to know what each parameter corresponds to.

sayMyFullName(firstName = "Chike", middleName = "Nnamdi", lastName = "Mgbemena")

In the second function call above, we supplied the parameter names before the argument values. You can see that this function call is clearer and more readable than the previous one. This way of calling functions helps reduce the possibility of bugs that can happen when arguments of the same type are swapped by mistake.

The caller can also alter the order of the parameters using named parameters. For example:

sayMyFullName(lastName = "Mgbemena", middleName = "Nnamdi", firstName = "Chike") // will still compile

In the code above, we swapped the argument position of the firstName with the lastName. The argument order doesn't matter with named parameters because the compiler will map each of them to the right function parameter.

Default Parameters

In Kotlin, we can give a function default values for any of its parameters. These default values are used if nothing is assigned to the arguments during the function call. To do this in Java, we'd have to create different overloaded methods.

Here, in our calCircumference() method, we modified the method by adding a default value for the pi parameter—Math.PI, a constant from the java.lang.Math package. 

fun calCircumference(radius: Double, pi: Double = Math.PI): Double = (2 * pi) * radius

When we call this function, we can either pass our approximated value for pi or use the default. 

print(calCircumference(24.0)) // used default value for PI and prints 150.79644737231007
print(calCircumference(24.0, 3.14)) // passed value for PI and prints 150.72

Let's see another example.

fun printName(firstName: String, middleName: String = "N/A", lastName: String) {
    println("first name: $firstName - middle name: $middleName - last name: $lastName")
}

In the following code, we tried to call the function, but it won't compile:

printName("Chike", "Mgbemena") // won't compile

In the function call above, I'm passing my first name and last name to the function, and hoping to use the default value for the middle name. But this won't compile because the compiler is confused. It doesn't know what the argument "Mgbemena" is for—is it for the middleName or the lastName parameter? 

To solve this issue, we can combine named parameters and default parameters. 

printName("Chike", lastName = "Mgbemena") // will now compile

Java Interoperability

Given that Java doesn't support default parameter values in methods, you'll need to specify all the parameter values explicitly when you call a Kotlin function from Java. But Kotlin provides us with the functionality to make it easier for the Java callers by annotating the Kotlin function with @JvmOverloads. This annotation will instruct the Kotlin compiler to generate the Java overloaded functions for us.

In the following example, we annotated the calCirumference() function with @JvmOverloads.

@JvmOverloads
fun calCircumference(radius: Double, pi: Double = Math.PI): Double = (2 * pi) * radius

The following code was generated by the Kotlin compiler so that Java callers can then choose which one to call.

// Java
double calCircumference(double radius, double pi);
double calCircumference(double radius);

In the last generated Java method definition, the pi parameter was omitted. This means that the method will use the default pi value.

Unlimited Arguments

In Java, we can create a method to receive an unspecified number of arguments by including an ellipsis (...) after a type in the method's parameter list. This concept is also supported by Kotlin functions with the use of the vararg modifier followed by the parameter name.

fun printInts(vararg ints: Int): Unit {
    for (n in ints) {
        print("$n\t")
    }
}
printInts(1, 2, 3, 4, 5, 6) // will print 1    2	3	4	5	6

The vararg modifier allows callers to pass in a comma-separated list of arguments. Behind the scenes, this list of arguments will be wrapped into an array. 

When a function has multiple parameters, the vararg parameter is typically the last one. It is also possible to have parameters after the vararg, but you'll need to use named parameters to specify them when you call the function. 

fun printNumbers(myDouble: Double, myFloat: Float, vararg ints: Int) {
    println(myDouble)
    println(myFloat)
    for (n in ints) {
        print("$n\t")
    }
}
printNumbers(1.34, 4.4F, 2, 3, 4, 5, 6) // will compile

For example, in the code above, the parameter with the vararg modifier is in the last position in a multiple parameters list (this is what we usually do). But what if we don't want it in the last position? In the following example, it is in the second position.

fun printNumbers(myDouble: Double, vararg ints: Int, myFloat: Float) {
    println(myDouble)
    println(myFloat)
    for (n in ints) {
        print("$n\t")
    }
}
printNumbers(1.34, 2, 3, 4, 5, 6, myFloat = 4.4F) // will compile
printNumbers(1.34, ints = 2, 3, 4, 5, 6, myFloat = 4.4F) // will not compile
printNumbers(myDouble = 1.34, ints = 2, 3, 4, 5, 6, myFloat = 4.4F) // will also not
compile

As you can observe in the updated code above, we used named arguments on the last parameter to solve this.

Spread Operator

Let's say we want to pass an array of integers to our printNumbers() function. The function expects the values to be unrolled into a list of parameters, though. If you try to pass the array directly into printNumbers(), you'll see that it won't compile. 

val intsArray: IntArray = intArrayOf(1, 3, 4, 5)
printNumbers(1.34, intsArray, myFloat = 4.4F) // won't compile

To solve this problem, we need to use the spread operator *. This operator will unpack the array and then pass the individual elements as arguments into the function for us. 

val intsArray: IntArray = intArrayOf(1, 3, 4, 5)
printNumbers(1.34, *intsArray, myFloat = 4.4F) // will now compile

By inserting the spread operator * in front of intsArray in the function's arguments list, the code now compiles and produces the same result as if we had passed the elements of intsArray as a comma-separated list of arguments. 

Return Multiple Values

Sometimes we want to return multiple values from a function. One way is to use the Pair type in Kotlin to create a Pair and then return it. This Pair structure encloses two values that can later be accessed. This Kotlin type can accept any types you supply its constructor. And, what's more, the two types don't even need to be the same. 

fun getUserNameAndState(id: Int): Pair<String?, String?> {
    require(id > 0, { "Error: id is less than 0" })

    val userNames: Map<Int, String> = mapOf(101 to "Chike", 102 to "Segun", 104 to "Jane")
    val userStates: Map<Int, String> = mapOf(101 to "Lagos", 102 to "Imo", 104 to "Enugu")

    val userName = userNames[id]
    val userState = userStates[id]
    return Pair(userName, userState)
}

In the function above, we constructed a new Pair by passing the userName and userState variables as the first and second arguments respectively to its constructor, and then returned this Pair to the caller.

Another thing to note is that we used a function called require() in the getUserNameAndState() function. This helper function from the standard library is used to give our function callers a precondition to meet, or else an IllegalArgumentException will be thrown (we'll discuss Exceptions in Kotlin in a future post). The optional second argument to require() is a function literal returning a message to be displayed if the exception is thrown. For example, calling the getUserNameAndState() function and passing -1 as an argument to it will trigger:

IntelliJ IDEA code execution resultIntelliJ IDEA code execution resultIntelliJ IDEA code execution result 

Retrieving Data From Pair

val userNameAndStatePair: Pair<String?, String?> = getUserNameAndState(101)
println(userNameAndStatePair.first) // Chike
println(userNameAndStatePair.second) // Lagos

In the code above, we accessed the first and second values from the Pair type by using its first and second properties.

However, there is a better way of doing this: destructuring.

val (name, state) = getUserNameAndState(101)
println(name) // Chike
println(state) // Lagos

What we have done in the updated code above is to directly assign the first and second values of the returned Pair type to the variables name and state respectively. This feature is called destructuring declaration.

Triple Return Values and Beyond

Now, what if you want to return three values at once? Kotlin provides us with another useful type called Triple.

fun getUserNameStateAndAge(id: Int): Triple<String?, String?, Int> {
    require(id > 0, { "id is less than 0" })
    
    val userNames: Map<Int, String> = mapOf(101 to "Chike", 102 to "Segun", 104 to "Jane")
    val userStates: Map<Int, String> = mapOf(101 to "Lagos", 102 to "Imo", 104 to "Enugu")
    
    val userName = userNames[id]
    val userState = userStates[id]
    val userAge = 6
    return Triple(userNames[id], userStates[id], userAge)
}

val (name, state, age) = getUserNameStateAndAge(101)
println(name) // Chike
println(state) // Lagos
println(age) // 6

I am sure some of you are wondering what to do if you want to return more than three values. The answer for that will be in a later post, when we discuss Kotlin's data classes.

Conclusion

In this tutorial, you learned about the packages and basic functions in the Kotlin programming language. In the next tutorial in the Kotlin From Scratch series, you'll learn more about functions in Kotlin. See you soon!

To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts+!


文章来源: https://code.tutsplus.com/tutorials/kotlin-from-scratch-packages-basic-functions--cms-29445
如有侵权请联系:admin#unsafe.sh