简介

在上一节,我们对Kotlin中函数的相关知识有了大致的了解,本章节我们将去了解一些Kotlin中的作用域函数。

目录

  1. let:处理可空对象,链式操作
  2. run:对象配置 + 计算返回值
  3. with:对非空对象执行多个操作
  4. apply:初始化对象配置
  5. also:附加操作(如打印日志)

Kotlin 中的作用域函数(Scope Functions)是 letrunwithapplyalso,它们可以简化对对象的操作,使代码更简洁

let:处理可空对象,链式操作

在 Kotlin 中,let 函数通过与 安全调用操作符 ?. 结合使用,优雅地处理可空对象。以下是详细解释:

  1. let 处理可空对象的基本原理

    • 语法结构:可空对象?.let{ ... }

    • 关键机制:

      • 如果对象 非空?. 会触发 let 代码块执行,对象作为参数(默认为 it)传递给 lambda

      • 如果对象 为空?. 会跳过 let 代码块,整个表达式返回 null ,且 lambda 中的代码 不会执行

        fun main() {
        val nullableString: String? = "Hello" // 在类型后面声明 ? 为 可空对象
        val length = nullableString?.let {
        println("执行 let: 对象非空, 内容为 $it") // 非空时才会打印 输出: 执行 let: 对象非空, 内容为 Hello
        it.length // 返回长度 (最后表达式的结果)
        }
        println("字符串长度: $length") // 输出: 字符串长度: 5 // 对象为空的时候
        val nullString: String? = null
        val nullResult = nullString?.let {
        println("因为对象为空,这里不会执行") // 被跳过
        it.length
        }
        println("空对象的结果是: $nullResult") // 输出: 空对象的结果是: null }
  2. 如果省略安全调用符 ?. (错误示范)

    直接调用 let (不加 ?.) 可能会导致空指针异常(NPE)

    fun main() {
    val str: String? = null
    str.let { // 编译警告:此处可能抛出 NPE!
    println(it!!.length) // 如果 str 为 null,
    }
    }
    /**
    * Exception in thread "main" java.lang.NullPointerException
    * at MainKt.main(main.kt:4)
    * at MainKt.main(main.kt)
    * */

    所以必须使用 ?.let 处理可空对象

  3. 链式操作与默认值处理

    结合 Elvis 操作符 ?:,可以为 let 的返回结果提供默认值:

    fun main() {
    val input: String? = null
    val processed = input?.let {
    it.uppercase() // 非空时转换成大写
    } ?: "DEFAULT" // 如何 input 为 null 返回 'DEFAULT'
    println(processed) // 输出: DEFAULT
    }
  4. 经典使用场景

    • 避免空检查嵌套

      data class User(val name: String = "")
      
      fun main() {
      val user = User("NPC")
      // 传统空检查(繁琐)
      if (user != null) {
      if (user.name != null) {
      println(user.name.length) // 输出: 3
      }
      }
      // 使用 ?.let(简洁)
      user?.name?.let { println(it.length) } // 输出: 3
      }
    • 数据转换

      fun main() {
      val number: Int? = "123".toIntOrNull() println(number) // 输出: 123 val squared = number?.let { it * it } // 非空时计算平方,否则返回 null println(squared) // 输出: 15129
      }
    • 副作用操作(如打印日志)

      fun main() {
      val data = "Hello Kotlin"
      data?.let {
      println("处理数据: $it")
      }
      }

run:对象配置 + 计算返回值

在 Kotlin 中,run 是一个灵活的作用域函数,它有两种形式:扩展函数非扩展函数。它的核心用途是在对象的上下文中执行代码块,并返回 lambda 表达式的结果。以下是 run 的详细讲解

  1. run 的两种形式

    • 形式1: 拓展函数(对象引用)

      对象.run {
      // 代码块:通过 this 访问对象
      // 最后一行作为返回值
      }
      • 上下文: 代码内使用 this 引用对象(可省略)

      • 返回值: lambda 的最后一行结果

    • 形式2: 非拓展函数(独立作用域)

      run {
      // 独立代码块(无需对象)
      // 最后一行作为返回值
      }
      • 用途: 创建一个临时作用域,避免变量污染外部环境
  2. 示例代码

    • 扩展函数(操作对象并返回结果)

      Car 对象上下文中配置属性,并返回一个描述状态的字符串。

      data class Car(var speed: Int = 0, var isEngineOn: Boolean = false)
      
      fun main() {
      val carStatus = Car().run {
      this.speed = 100 // 直接访问属性(this 可省略)
      isEngineOn = true // 修改对象状态
      "车速: $speed km/h,引擎状态: ${if (isEngineOn) "开启" else "关闭"}" // 返回字符串
      } println(carStatus) // 输出: 车速: 100 km/h,引擎状态: 开启
      }
    • 处理可空对象(结合空安全调用?.run)

      仅在对象非空时执行代码块,避免空指针异常。

      fun printLengthIfNotNull(input: String?) {
      input?.run {
      println("字符串内容: $this, 长度: $length") // this 指代 input 对象
      } ?: println("输入为空")
      }
      fun main() {
      printLengthIfNotNull("Hello, Kotlin") // 输出: 字符串内容: Hello, Kotlin, 长度: 13
      printLengthIfNotNull(null) // 输出: 输入为空
      }
    • 非扩展形式(独立作用域)

      封装临时计算逻辑,避免变量 ab 泄漏到外部作用域。

      fun main() {
      val result = run {
      val a = 10
      val b = 20
      a + b // 返回计算结果
      }
      println("计算结果:$result") // 输出:计算结果:30
      }
  3. 高级用法

    • 链式调用多个 run

      fun main() {
      val message = "Kotlin"
      .run { uppercase() } // 转换为大写
      .run { "Message: $this" } // 添加前缀
      println(message) // 输出: Message: KOTLIN
      }
    • apply 结合使用

      data class Config(var host: String = "", var port: Int = 0)
      
      fun main() {
      val config = Config().apply {
      host = "127.0.0.1" // 初始化配置
      port = 8080
      }.run {
      "服务器地址: $host:$port" // 转换为连接字符串
      }
      println(config) // 输出: 服务器地址: 127.0.0.1:8080
      }

apply:初始化对象配置

在 Kotlin 中,apply 是一个常用的作用域函数,专门用于对象的初始化或配置。它通过简洁的语法让你在一个代码块中完成对对象属性的设置,并最终返回对象本身,以下是 apply 的详细讲解

  1. 基本语法

    val 对象 = 原始对象.apply {
    // 在此配置对象的属性或调用方法
    this.property = value // this 可省略
    method()
    // ...
    }
    // apply 返回原始对象,可以继续操作
  2. 示例代码

    • 初始化对象属性

      data class Person(var name: String = "", var age: Int = 0)
      
      fun main() {
      val person = Person().apply {
      name = "Alice" // 等价于 this.name = "Alice"
      age = 30 // 直接访问属性, this 可省略
      }
      println(person) // 输出: Person(name=Alice, age=30)
      }
    • 链式配置多个属性

      class Car {
      var brand: String = ""
      var speed: Int = 0
      fun start() { println("$brand 启动,速度: $speed km/h") } }
      fun main() {
      var car = Car()
      .apply { brand = "XiaoMi SU7" }
      .apply { speed = 200 }
      .apply { start() } // 输出: XiaoMi SU7 启动,速度: 200 km/h
      }
    • 替代 Builder 模式

      // 传统 Builder 模式 vs apply 简化
      class Dialog {
      var title: String = ""
      var message: String = "" fun show() { println("显示对话框:$title - $message") }
      } fun main() {
      // 传统方式
      val dialog1 = Dialog()
      dialog1.title = "提示"
      dialog1.message = "欢迎使用 Kotlin"
      dialog1.show() // 使用 apply(更简洁)
      val dialog2 = Dialog().apply {
      title = "提示"
      message = "欢迎使用 Kotlin"
      show() // 直接调用方法
      }
      }
    • 处理可空对象

      fun configureNullableObject() {
      val nullableConfig: String? = null
      nullableConfig?.apply {
      println("配置非空对象:$this") // 此处不会执行
      } ?: println("对象为空") // 输出:对象为空
      } fun main() {
      configureNullableObject()
      }
  3. 常见误区

    • 错误:在 apply 中返回其他值

      data class Person (var name: String = "",var age: Int = 0)
      
      fun main() {
      val person = Person().apply {
      name = "Bob"
      "这是一个错误示例" // 这行代码无效
      }
      println(person)
      }
    • 正确的做法是: 若需要返回计算结果,应使用 run

      data class Person (var name: String = "",var age: Int = 0)
      
      fun main() {
      val person = Person().run {
      name = "Bob"
      "姓名: $name"
      }
      println(person) // 输出: 姓名: Bob
      }
  4. 高级用法

    • 链式调用多个作用域函数

      data class File(var absolutePath: String = "") {
      fun readText() {
      // ... 读取文件操作
      println("读取文件操作")
      }
      } fun createNewFile() {
      // ... 创建文件操作
      println("创建文件操作")
      }
      fun main() {
      File("data.txt")
      .apply { createNewFile() }
      .also { println("文件路径: ${it.absolutePath}") }
      .readText()
      }
    • 结合 takeIf 过滤条件

      data class Person(var name: String = "", var age: Int = 0)
      fun main() {
      val validPerson = Person().apply {
      name = "Charlie"
      age = 25
      }.takeIf { it.age >= 18 } // 只保留成年人
      println(validPerson) // 输出:Person(name=Charlie, age=25)
      }

with:对非空对象执行多个操作

在 Kotlin 中,with 是一个作用域函数,用于对已有对象执行多个操作,它通过将对象作为上下文(this)传递到代码块中,让代码更集中、更易读。以下是 with 的详细讲解

  1. 基本语法

    val 结果 = with(对象) {
    // 在此直接访问对象的属性和方法(this 可省略)
    操作1
    操作2
    ...
    最后一行作为返回值
    }
  2. 示例代码

    • 批量操作对象属性

      data class User(var name: String = "", var age: Int = 0)
      
      fun main() {
      val user = User("Alice", 25)
      val info = with(user) {
      name = "Bob" // 等价于 this.name = "Bob"
      age += 5 // 修改属性
      "更新后:$name, $age 岁" // 返回字符串
      }
      println(info) // 输出:更新后:Bob, 30 岁
      println(user) // 输出:User(name=Bob, age=30)
      }
    • 执行计算并返回结果

      class Rectangle(val width: Int, val height: Int) {
      fun area() = width * height
      } fun main() {
      val rect = Rectangle(10, 20)
      val result = with(rect) {
      val perimeter = 2 * (width + height) // 访问属性
      "面积: ${area()}, 周长: $perimeter" // 调用方法,返回字符串
      }
      println(result) // 输出:面积: 200, 周长: 60
      }
    • 处理集合操作

      fun main() {
      val numbers = listOf(1, 2, 3, 4, 5)
      val summary = with(numbers) {
      val sum = sum()
      val avg = average()
      "总和: $sum, 平均值: $avg" // 返回统计结果
      }
      println(summary) // 输出:总和: 15, 平均值: 3.0
      }
  3. 常见误区

    • 错误:对可空对象直接使用 with

      data class Person(var name: String = "", var age: Int = 0)
      
      fun main() {
      val person: Person? = null
      with(person) {
      println(name) // 编译报错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Person?
      }
      }
    • 正确做法

      data class Person(var name: String = "", var age: Int = 0)
      
      fun main() {
      val user: Person? = null
      with(user ?: return) { // 如果 user 为 null,提前退出
      println(name)
      }
      }
  4. 经典应用场景

    • 集中配置对象属性

      data class Person(var name: String = "", var age: Int = 0) {
      fun sout() {
      println("Name: $name, Age: $age")
      }
      } fun main() {
      val person = Person()
      with(person) {
      name = "NPC"
      age = 24
      sout() // 输出: Name: NPC, Age: 24
      } }
    • 数据转换与计算

      fun main() {
      val list = listOf(1, 2, 3)
      val squaredSum = with(list) {
      map { it * it }.sum()
      }
      println(squaredSum) // 输出: 14
      }
    • 简化多步操作

      data class File(var fileName: String ="") {
      fun createNewFile() {
      println("创建文件 $fileName")
      }
      fun exists(): Boolean {
      println("判断 $fileName 文件是否存在")
      return false
      }
      fun readText(): String {
      return "读取文件 $fileName"
      }
      } fun main() {
      val file = File("data.txt")
      val content = with(file) {
      if (!exists()) createNewFile()
      readText().uppercase()
      }
      println(content) // 输出: 读取文件 DATA.TXT
      }

also:附加操作(如打印日志)

在 Kotlin 中,also 是一个作用域函数,专注于执行附加操作(如日志记录、验证、调试),同时保留对象本身,并支持链式调用。它不会修改对象,但允许在对象操作流程中插入“副作用”。以下是 also 的详细讲解

  1. 基本语法

    val 对象 = 原始对象.also {
    // 通过 it 访问对象,执行附加操作
    // 返回原始对象(无论代码块内做了什么)
    }
  2. 示例代码

    • 打印日志(调试中间状态)

      data class User(var name: String, var age: Int)
      
      fun main() {
      val user = User("Alice", 25)
      .also { println("初始化后:$it") } // 输出:初始化后:User(name=Alice, age=25)
      .apply { age += 5 }
      .also { println("修改年龄后:$it") } // 输出:修改年龄后:User(name=Alice, age=30) println("最终结果:$user") // 输出:最终结果:User(name=Alice, age=30)
      }
    • 数据验证

      fun processOrder(order: Order?) {
      order?.also {
      require(it.amount > 0) { "订单金额必须大于 0" }
      requireNotNull(it.customer) { "订单必须有客户信息" }
      }?.also {
      println("开始处理订单:${it.id}") // 验证通过后执行
      }
      } data class Order(val id: String, var amount: Double, var customer: String?) fun main() {
      val validOrder = Order("123", 100.0, "Alice")
      processOrder(validOrder) // 输出:开始处理订单:123 val invalidOrder = Order("456", -50.0, null)
      processOrder(invalidOrder) // 抛出 IllegalArgumentException
      }
    • 链式调用中插入操作

      fun main() {
      val list = mutableListOf(1, 2, 3)
      .also { it.add(4) } // 添加元素
      .also { it.remove(0) } // 删除第一个元素(实际无 0,此操作为演示)
      .also { println("当前列表:$it") } // 输出:当前列表:[2, 3, 4] println(list) // 输出:[2, 3, 4]
      }
  3. 常见误区

    • 错误: 在 also 中尝试返回其他值

      data class User(var name: String, var age: Int)
      
      fun main() {
      // 错误!also 永远返回原始对象,忽略代码块内的返回值
      val user = User("Alice", 25).also {
      "无效的返回值" // 这行代码无意义
      }
      println(user) // 输出:User(name=Alice, age=25)
      }
    • 正确做法: 若需返回计算结果,应使用 letrun

      data class User(var name: String, var age: Int)
      
      fun main() {
      val info = User("Alice", 25).let {
      "${it.name} 的年龄是 ${it.age}" // 返回字符串
      }
      println(info) // 输出:Alice 的年龄是 25
      }
  4. 高级用法

    • 结合 takeIf 进行条件过滤

      data class User(var name: String, var age: Int)
      
      fun createUser(name: String?, age: Int): User? {
      return name?.let {
      User(it, age)
      }?.takeIf { it.age >= 18 } // 只保留成年人
      ?.also { println("用户创建成功:$it") } // 记录日志
      } fun main() {
      val user = createUser("Bob", 20) // 输出:用户创建成功:User(name=Bob, age=20)
      println(user) // 输出:User(name=Bob, age=20)
      }
    • 再集合操作中跟踪流程

      fun main() {
      val numbers = (1..10)
      .filter { it % 2 == 0 }
      .also { println("过滤后的偶数:$it") } // 输出:过滤后的偶数:[2, 4, 6, 8, 10]
      .map { it * it }
      .also { println("平方结果:$it") } // 输出:平方结果:[4, 16, 36, 64, 100]
      }

核心对比表

函数 上下文对象引用 返回值 典型场景 空安全支持
let it Lambda结果 可空对象处理、数据转换 需配合?.object?.let
run this Lambda结果 对象配置 + 返回结果计算、独立作用域 需配合?.object?.run
apply this 对象本身 对象初始化(链式配置属性) 需配合?.object?.apply
with this Lambda结果 对已有非空对象批量操作 需自行处理空安全
also it 对象本身 附加操作(日志、验证)、链式调用中的中间操作 需配合?.object?.also
  • 上下文对象引用

    data class Car(var speed: Int = 0) {
    fun accelerate() { speed += 10 }
    } fun main() {
    val car = Car().apply {
    speed = 100 // 直接访问属性(this 可省略)
    accelerate() // 直接调用方法
    }
    println(car) // 输出:Car(speed=110)
    }
  • it (显示引用):letalso

    fun main() {
    val list = mutableListOf(1, 2, 3)
    .also { it.add(4) } // 显式用 it 操作
    .let { it.joinToString("-") } // 转换为字符串
    println(list) // 输出:1-2-3-4
    }
  • 返回对象本身:applyalso

    data class Button(var text: String = "", var textSize: Float = 0.0f)
    
    fun main() {
    // apply 返回对象本身,适合链式配置
    val button = Button().apply {
    text = "Submit"
    textSize = 16f
    }
    // also 返回对象本身,适合插入附加操作
    button.also { println("按钮已配置:$it") } // 输出: 按钮已配置:Button(text=Submit, textSize=16.0) }
  • 返回 Lambda 结果:letrunalso

    data class Car(var speed: Int = 0) {
    fun accelerate() { speed += 10 }
    } fun main() {
    // let 返回转换后的数据
    val length = "Kotlin".let { it.length } println(length) // 输出: 6 // run 返回计算结果
    val area = Car(200).run { speed * 2 } println(area) // 输出: 400 // with 返回处理后的结果
    val info = with(Car()) {
    speed = 100
    "车速:$speed km/h"
    } println(info) // 输出: 车速: 100 km/h
    }
  • 需配合 ?. : letrunapplyalso

    fun main() {
    val nullableString: String? = null nullableString?.let {
    println(it.length) // 非空时执行
    } ?: println("字符串为空") nullableString?.apply {
    println(length) // 非空时执行
    }
    }
  • 需自行处理: with

    data class User(var name: String = "", var age: Int = 0)
    
    fun main() {
    val user: User? = User()
    user?.let {
    with(it) { // 确保非空后使用 with
    name = "Alice"
    age = 30
    }
    }
    println(user) // 输出: User(name=Alice, age=30)
    }

选择指南

  1. 是否需要返回对象本身?

    • 是 → applyalso(根据是否需要显式 it)。
    • 否 → letrunwith(根据是否需要隐式 this)。
  2. 是否需要处理可空对象?
    • 是 → ?.let?.run?.apply?.also
    • 否 → with 或直接使用其他函数。
  3. 是否需要链式调用中的中间操作?
    • 是 → also(如记录日志)。
    • 否 → 根据返回值需求选择。

混合使用示例

// 链式调用:初始化、验证、转换
data class Product(var name: String = "", var price: Double = 0.0) fun main() {
val productInfo = Product()
.apply {
name = "手机"
price = 2999.0
}
.also {
require(it.price > 0) { "价格必须大于 0" }
println("商品已初始化:$it")
}
.let {
"${it.name} 价格:${it.price} 元" // 返回字符串
} println(productInfo) // 输出:手机 价格:2999.0 元
}

学习Kotlin语法(四)的更多相关文章

  1. Python学习系列(四)Python 入门语法规则2

    Python学习系列(四)Python 入门语法规则2 2017-4-3 09:18:04 编码和解码 Unicode.gbk,utf8之间的关系 2.对于py2.7, 如果utf8>gbk, ...

  2. 我的MYSQL学习心得(四) 数据类型

    我的MYSQL学习心得(四) 数据类型 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(五) 运 ...

  3. ios -- 教你如何轻松学习Swift语法(一)

    目前随着公司开发模式的变更,swift也显得越发重要,相对来说,swift语言更加简洁,严谨.但对于我来说,感觉swift细节的处理很繁琐,可能是还没适应的缘故吧.基本每写一句代码,都要对变量的数据类 ...

  4. X-Cart 学习笔记(四)常见操作

    目录 X-Cart 学习笔记(一)了解和安装X-Cart X-Cart 学习笔记(二)X-Cart框架1 X-Cart 学习笔记(三)X-Cart框架2 X-Cart 学习笔记(四)常见操作 五.常见 ...

  5. WCF学习心得----(四)服务承载

    WCF学习心得----(四)服务承载 这一章节花费了好长的时间才整理个大概,主要原因是初次接触这个东西,在做练习实践的过程中,遇到了很多的问题,有些问题到目前还没有得以解决.所以在这一章节中,有一个承 ...

  6. 程序员带你学习安卓开发,十天快速入-对比C#学习java语法

    关注今日头条-做全栈攻城狮,学代码也要读书,爱全栈,更爱生活.提供程序员技术及生活指导干货. 如果你真想学习,请评论学过的每篇文章,记录学习的痕迹. 请把所有教程文章中所提及的代码,最少敲写三遍,达到 ...

  7. VSTO学习笔记(四)从SharePoint 2010中下载文件

    原文:VSTO学习笔记(四)从SharePoint 2010中下载文件 上一次我们开发了一个简单的64位COM加载项,虽然功能很简单,但是包括了开发一个64位COM加载项的大部分过程.本次我们来给CO ...

  8. 浅谈Kotlin(四):控制流

    浅谈Kotlin(一):简介及Android Studio中配置 浅谈Kotlin(二):基本类型.基本语法.代码风格 浅谈Kotlin(三):类 浅谈Kotlin(四):控制流 本篇介绍Kotlin ...

  9. 学习HTML 第四节.插入图像

    学习HTML 第四节.插入图像 全是文字的网页太枯燥了吧,我们来搞个图片上去! <!DOCTYPE html><html><head><meta charse ...

  10. 推荐两份学习 Kotlin 和机器学习的资料

    最近 Kotlin 和人工智能比较火,有不少同学留言问我怎么学习 Kotlin,怎么学习机器学习,今天就给大家推荐两份不错的学习资料. 1. Kotlin 学习资料其实,在我看来最好的学习资料就是 K ...

随机推荐

  1. Windows bat批处理文件结束某个程序进程,删除文件夹

    Windows bat批处理文件结束某个程序进程,删除文件夹 bat文件内容: @echo off setlocal :: 要结束的应用程序进程名 set "PROCESS_NAME=助手. ...

  2. 并发编程之 ConcurrentLinkedQueue 源码

    文章目录1 ConcurrentLinkedQueue的概述2 ConcurrentLinkedQueue的实现2.1 基本结构2.2 构造器2.2.1 ConcurrentLinkedQueue2. ...

  3. w3cschool-Netty 实战精髓篇2

    https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-adkv28bm.html 研究Netty中的 ...

  4. 【隐私计算笔谈】MPC系列专题(十):安全多方计算下的集合运算

    学习&转载文章:[隐私计算笔谈]MPC系列专题(十):安全多方计算下的集合运算 集合运算 集合可以通俗地描述为确定的一堆东西.如有一个集合\(\),一个元素\(\)要么属于集合\(\),记做\ ...

  5. DP(优化)

    史不分好坏.是史就应该冲进. 细节见其他题解. P10538 首先建出部分分 sub1 的图,发现是 DAG,于是设点为状态,即即将乘坐 \(j\) 车的最小代价 \(f_j\).这样的转移就是枚举上 ...

  6. linux安装flink

    参考链接https://blog.csdn.net/boling_cavalry/article/details/85038527   1.下载flink 在Flink官网下载,地址是:https:/ ...

  7. 同步工具-腾讯EMR表治理工具安装使用

    一.安装 1.root用户上传文件 cd wangrz -bey luoshu-1.0-bin.tar.gz 2.解压文件到服务目录 重新安装洛书需执行:rm -rf /usr/local/servi ...

  8. Flink流式数据缓冲后批量写入Clickhouse

    一.背景 对于clickhouse有过使用经验的开发者应该知道,ck的写入,最优应该是批量的写入.但是对于流式场景来说,每批写入的数据量都是不可控制的,如kafka,每批拉取的消息数量是不定的,fli ...

  9. vue-element-admin改为从后台获取菜单

    一.修改文件\src\router\index.js 文件的asyncRoutes清理为 export const asyncRoutes = [ { path: '*', redirect: '/4 ...

  10. Luogu P11628 WC2025 猫粮 题解 [ 绿 ] [ 贪心 ] [ adhoc ] [ 鸽巢原理 ]

    猫粮:WC 诈骗题.我竟然能切 WC 的 T3 也是逆天了. 话说切了猫粮能变成猫娘吗 qwq. 思路 首先题目里有下面几点关键的性质: 所有猫粮质量总和等于所有猫要吃的质量总和. 优质的有 \(n\ ...