练习:Kotlin 基础知识

1. 开始之前

既然你已经努力学习了 Kotlin 编程的基础知识,现在是时候将所学知识付诸实践了。

这些练习将测试你对所学概念的理解。它们基于真实世界的使用场景,其中一些你可能作为用户曾遇到过。

按照说明在 Kotlin Playground 中找到每个练习的解决方案。如果你遇到困难,有些练习提供提示可以帮助你。每个练习的解决方案代码在文末提供,但建议你在查看答案之前先完成练习。

按照自己舒适的节奏完成练习。练习有预估时长,但这只是预估,你无需严格遵守。花足够的时间仔细思考解决每个问题。解决方案只是解决练习的一种方法,因此请随意尝试,怎么感觉舒服就怎么来。

前提条件

你需要准备什么

  • Kotlin Playground

2. 移动通知

通常,你的手机会为你提供通知摘要。

在以下代码片段中提供的初始代码中,编写一个程序,根据收到的通知数量打印摘要消息。消息应包含

  • 当通知数量少于 100 时,显示确切的通知数量。
  • 当通知数量为 100 或更多时,显示 99+ 作为通知数量。
fun main() {
    val morningNotification = 51
    val eveningNotification = 135
    
    printNotificationSummary(morningNotification)
    printNotificationSummary(eveningNotification)
}


fun printNotificationSummary(numberOfMessages: Int) {
    // Fill in the code.
}

完善 printNotificationSummary() 函数,使程序打印以下行

You have 51 notifications.
Your phone is blowing up! You have 99+ notifications.

3. 电影票价

电影票价通常根据观影者的年龄而有所不同。

在以下代码片段中提供的初始代码中,编写一个程序来计算这些基于年龄的票价

  • 12 岁及以下人群的儿童票价为 15 美元。
  • 13 岁至 60 岁人群的标准票价为 30 美元。周一,此年龄段的标准票价优惠至 25 美元。
  • 61 岁及以上人群的老年票价为 20 美元。假设观影者的最大年龄为 100 岁。
  • 当用户输入的年龄不在年龄规定范围内时,-1 表示价格无效。
fun main() {
    val child = 5
    val adult = 28
    val senior = 87
    
    val isMonday = true
    
    println("The movie ticket price for a person aged $child is \$${ticketPrice(child, isMonday)}.")
    println("The movie ticket price for a person aged $adult is \$${ticketPrice(adult, isMonday)}.")
    println("The movie ticket price for a person aged $senior is \$${ticketPrice(senior, isMonday)}.")
}

fun ticketPrice(age: Int, isMonday: Boolean): Int {
    // Fill in the code.
}

完善 ticketPrice() 函数,使程序打印以下行

The movie ticket price for a person aged 5 is $15.
The movie ticket price for a person aged 28 is $25.
The movie ticket price for a person aged 87 is $20.

4. 温度转换器

世界上主要使用三种温度标度:摄氏度、华氏度和开尔文。

在以下代码片段中提供的初始代码中,编写一个程序,使用以下公式将温度从一个标度转换为另一个标度

  • 摄氏度转华氏度:° F = 9/5 (° C) + 32
  • 开尔文转摄氏度:° C = K - 273.15
  • 华氏度转开尔文:K = 5/9 (° F - 32) + 273.15

请注意,String.format("%.2f", /* measurement */ ) 方法用于将数字转换为保留 2 位小数的 String 类型。

fun main() {
    // Fill in the code.
}


fun printFinalTemperature(
    initialMeasurement: Double, 
    initialUnit: String, 
    finalUnit: String, 
    conversionFormula: (Double) -> Double
) {
    val finalMeasurement = String.format("%.2f", conversionFormula(initialMeasurement)) // two decimal places
    println("$initialMeasurement degrees $initialUnit is $finalMeasurement degrees $finalUnit.")
}

完善 main() 函数,使其调用 printFinalTemperature() 函数并打印以下行。你需要为温度和转换公式传递参数。提示:你可能需要使用 Double 值来避免在除法运算中出现 Integer 截断。

27.0 degrees Celsius is 80.60 degrees Fahrenheit.
350.0 degrees Kelvin is 76.85 degrees Celsius.
10.0 degrees Fahrenheit is 260.93 degrees Kelvin.

5. 歌曲目录

假设你需要创建一个音乐播放器应用。

创建一个能够代表歌曲结构的类。Song 类必须包含以下代码元素

  • 歌曲标题、艺术家、发行年份和播放次数的属性
  • 一个指示歌曲是否受欢迎的属性。如果播放次数少于 1,000,则认为它不受欢迎。
  • 一个方法,用于按照以下格式打印歌曲描述

"[标题],由 [艺术家] 演绎,发行于 [发行年份]。"

6. 网络个人资料

通常,你需要填写在线网站的个人资料,其中包含必填和非必填字段。例如,你可以添加你的个人信息,并链接到推荐你注册个人资料的其他人。

在以下代码片段中提供的初始代码中,编写一个程序来打印一个人的个人资料详情。

fun main() {    
    val amanda = Person("Amanda", 33, "play tennis", null)
    val atiqah = Person("Atiqah", 28, "climb", amanda)
    
    amanda.showProfile()
    atiqah.showProfile()
}


class Person(val name: String, val age: Int, val hobby: String?, val referrer: Person?) {
    fun showProfile() {
       // Fill in code 
    }
}

完善 showProfile() 函数,使程序打印以下行

Name: Amanda
Age: 33
Likes to play tennis. Doesn't have a referrer.

Name: Atiqah
Age: 28
Likes to climb. Has a referrer named Amanda, who likes to play tennis.

7. 折叠屏手机

通常,按下电源按钮时,手机屏幕会亮起和熄灭。相比之下,如果折叠屏手机处于折叠状态,按下电源按钮时,主内屏不会亮起。

在以下代码片段中提供的初始代码中,编写一个继承自 Phone 类的 FoldablePhone 类。它应包含以下内容

  • 一个指示手机是否处于折叠状态的属性。
  • Phone 类不同的 switchOn() 函数行为,使其仅在手机未折叠时才打开屏幕。
  • 改变折叠状态的方法。
class Phone(var isScreenLightOn: Boolean = false){
    fun switchOn() {
        isScreenLightOn = true
    }
    
    fun switchOff() {
        isScreenLightOn = false
    }
    
    fun checkPhoneScreenLight() {
        val phoneScreenLight = if (isScreenLightOn) "on" else "off"
        println("The phone screen's light is $phoneScreenLight.")
    }
}

8. 特殊拍卖

通常在拍卖中,出价最高者决定物品的价格。在本次特殊拍卖中,如果某物品没有竞标者,该物品将自动以最低价格出售给拍卖行。

在以下代码片段中提供的初始代码中,提供了一个接受可空 Bid? 类型作为参数的 auctionPrice() 函数

fun main() {
    val winningBid = Bid(5000, "Private Collector")
    
    println("Item A is sold at ${auctionPrice(winningBid, 2000)}.")
    println("Item B is sold at ${auctionPrice(null, 3000)}.")
}

class Bid(val amount: Int, val bidder: String)
 
fun auctionPrice(bid: Bid?, minimumPrice: Int): Int {
   // Fill in the code.
}

完善 auctionPrice() 函数,使程序打印以下行

Item A is sold at 5000.
Item B is sold at 3000.

9. 解决方案代码

移动通知

该解决方案使用 if/else 语句根据收到的通知消息数量打印相应的通知摘要消息

fun main() {
    val morningNotification = 51
    val eveningNotification = 135
    
    printNotificationSummary(morningNotification)
    printNotificationSummary(eveningNotification)
}


fun printNotificationSummary(numberOfMessages: Int) {
    if (numberOfMessages < 100) {
        println("You have ${numberOfMessages} notifications.")
    } else {
        println("Your phone is blowing up! You have 99+ notifications.")
    }
}

电影票价

该解决方案使用 when 表达式根据观影者的年龄返回相应的票价。它还在 when 表达式的一个分支中使用简单的 if/else 表达式来添加标准票价的附加条件。

else 分支中的票价返回 -1 值,表示为 else 分支设置的价格无效。更好的实现方式是让 else 分支抛出异常。你将在后续单元中学习异常处理。

fun main() {
    val child = 5
    val adult = 28
    val senior = 87
    
    val isMonday = true
    
    println("The movie ticket price for a person aged $child is \$${ticketPrice(child, isMonday)}.")
    println("The movie ticket price for a person aged $adult is \$${ticketPrice(adult, isMonday)}.")
    println("The movie ticket price for a person aged $senior is \$${ticketPrice(senior, isMonday)}.")
}
 
fun ticketPrice(age: Int, isMonday: Boolean): Int {
    return when(age) {
        in 0..12 -> 15
        in 13..60 -> if (isMonday) 25 else 30
        in 61..100 -> 20
        else -> -1
    }
}

温度转换器

该解决方案要求你将函数作为参数传递给 printFinalTemperature() 函数。最简洁的解决方案是将 lambda 表达式作为参数传递,使用 it 参数引用代替参数名,并利用尾随 lambda 语法。

fun main() {    
        printFinalTemperature(27.0, "Celsius", "Fahrenheit") { 9.0 / 5.0 * it + 32 }
        printFinalTemperature(350.0, "Kelvin", "Celsius") { it - 273.15 }
        printFinalTemperature(10.0, "Fahrenheit", "Kelvin") { 5.0 / 9.0 * (it - 32) + 273.15 }
}


fun printFinalTemperature(
    initialMeasurement: Double, 
    initialUnit: String, 
    finalUnit: String, 
    conversionFormula: (Double) -> Double
) {
    val finalMeasurement = String.format("%.2f", conversionFormula(initialMeasurement)) // two decimal places
    println("$initialMeasurement degrees $initialUnit is $finalMeasurement degrees $finalUnit.")
}

歌曲目录

该解决方案包含一个 Song 类,带有一个接受所有必需参数的默认构造函数。Song 类还有一个使用自定义 getter 函数的 isPopular 属性,以及一个打印自身描述的方法。你可以在 main() 函数中创建类的实例并调用其方法,以测试实现是否正确。书写 1_000_000 等大数字时可以使用下划线,使其更易读。

fun main() {    
    val brunoSong = Song("We Don't Talk About Bruno", "Encanto Cast", 2022, 1_000_000)
    brunoSong.printDescription()
    println(brunoSong.isPopular)
}


class Song(
    val title: String, 
    val artist: String, 
    val yearPublished: Int, 
    val playCount: Int
){
    val isPopular: Boolean
        get() = playCount >= 1000

    fun printDescription() {
        println("$title, performed by $artist, was released in $yearPublished.")
    }   
}

当你在实例的方法上调用 println() 函数时,程序可能会打印此输出

We Don't Talk About Bruno, performed by Encanto Cast, was released in 2022.
true

网络个人资料

该解决方案在各种 if/else 语句中包含空值检查,以便根据各种类属性是否为 null 来打印不同的文本

fun main() {    
    val amanda = Person("Amanda", 33, "play tennis", null)
    val atiqah = Person("Atiqah", 28, "climb", amanda)
    
    amanda.showProfile()
    atiqah.showProfile()
}


class Person(val name: String, val age: Int, val hobby: String?, val referrer: Person?) {
    fun showProfile() {
        println("Name: $name")
        println("Age: $age")
        if(hobby != null) {
            print("Likes to $hobby. ")
        }
        if(referrer != null) {
            print("Has a referrer named ${referrer.name}")
            if(referrer.hobby != null) {
                print(", who likes to ${referrer.hobby}.")
            } else {
                print(".")
            }
        } else {
            print("Doesn't have a referrer.")
        }
        print("\n\n")
    }
}

折叠屏手机

要使 Phone 类成为父类,你需要在类名前添加 open 关键字来开放该类。要在 FoldablePhone 类中覆盖 switchOn() 方法,你需要在 Phone 类的方法前添加 open 关键字来开放该方法。

该解决方案包含一个 FoldablePhone 类,带有一个默认构造函数,其中包含 isFolded 参数的默认参数。 FoldablePhone 类还有两个方法用于将 isFolded 属性更改为 truefalse 值。它还覆盖了从 Phone 类继承的 switchOn() 方法。

你可以在 main() 函数中创建该类的实例并调用其方法,以测试实现是否正确。

open class Phone(var isScreenLightOn: Boolean = false){
    open fun switchOn() {
        isScreenLightOn = true
    }
    
    fun switchOff() {
        isScreenLightOn = false
    }
    
    fun checkPhoneScreenLight() {
        val phoneScreenLight = if (isScreenLightOn) "on" else "off"
        println("The phone screen's light is $phoneScreenLight.")
    }
}

class FoldablePhone(var isFolded: Boolean = true): Phone() {
    override fun switchOn() {
        if (!isFolded) {
            isScreenLightOn = true
        }
    }
    
    fun fold() {
        isFolded = true
    }
    
    fun unfold() {
        isFolded = false
    }
}

fun main() {    
    val newFoldablePhone = FoldablePhone()
    
    newFoldablePhone.switchOn()
    newFoldablePhone.checkPhoneScreenLight()
    newFoldablePhone.unfold()
    newFoldablePhone.switchOn()
    newFoldablePhone.checkPhoneScreenLight()
}

输出如下

The phone screen's light is off.
The phone screen's light is on.

特殊拍卖

该解决方案使用 ?. 安全调用运算符和 ?: Elvis 运算符来返回正确价格

fun main() {
    val winningBid = Bid(5000, "Private Collector")
    
    println("Item A is sold at ${auctionPrice(winningBid, 2000)}.")
    println("Item B is sold at ${auctionPrice(null, 3000)}.")
}

class Bid(val amount: Int, val bidder: String)

fun auctionPrice(bid: Bid?, minimumPrice: Int): Int {
    return bid?.amount ?: minimumPrice
}

10. 附加练习

如需更多 Kotlin 语言练习,请访问 JetBrains Academy 的 Kotlin Core 课程。要跳转到特定主题,请访问知识地图查看该课程涵盖的主题列表。