Android Kotlin:Kotlin数据类与密封类

一、开篇痛点:样板代码地狱与状态爆炸

在Android开发中,业务建模是最基础也最容易出错的工作。想象一下你正在开发一个电商应用,需要定义网络请求返回的商品详情DTO,以及订单状态流转:

java 复制代码
// Java时代的噩梦 - 商品DTO
public class ProductDTO {
    private String id;
    private String name;
    private BigDecimal price;
    private List<String> tags;
    
    // 构造函数、getter、setter、equals、hashCode、toString...
    // 50行样板代码,手抖写错一个equals就导致比价逻辑bug
    
    public ProductDTO(String id, String name, BigDecimal price, List<String> tags) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.tags = tags;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductDTO that = (ProductDTO) o;
        return Objects.equals(id, that.id) && 
               Objects.equals(name, that.name) && 
               Objects.equals(price, that.price);
    }
    
    // hashCode, toString, getters, setters... 
    // 每加一个字段就要改5个地方,维护成本极高
}

// 状态管理:用int常量+ Object混合数据,类型不安全
public static final int STATE_LOADING = 0;
public static final int STATE_SUCCESS = 1;
public static final int STATE_ERROR = 2;

public class OrderViewModel {
    private MutableLiveData<Integer> state = new MutableLiveData<>();
    private MutableLiveData<String> errorMessage = new MutableLiveData<>();
    private MutableLiveData<Order> orderData = new MutableLiveData<>();
    
    // 问题:状态与数据分离,无法保证一致性;Integer状态值可以随意传错
    public void loadOrder(String orderId) {
        state.setValue(STATE_LOADING);
        repository.getOrder(orderId, new Callback() {
            @Override
            public void onResponse(Order order) {
                state.setValue(STATE_SUCCESS);
                orderData.setValue(order);
            }
            @Override
            public void onFailure(String error) {
                state.setValue(STATE_ERROR);
                errorMessage.setValue(error);
            }
        });
    }
}

这段代码的问题显而易见:DTO维护成本极高 ,任何字段变更都需要同步修改equals、hashCode等多个方法;状态管理缺乏类型安全 ,状态和数据的对应关系全靠人工约定,容易出错;空安全无法保障,Java代码中随处可见的null检查或遗漏的NPE。

二、Kotlin解法:数据类与密封类的优雅重构

Kotlin通过data class和sealed class显著改变了这一局面。让我们逐步重构:

2.1 数据类重构DTO

kotlin 复制代码
// 文件路径: com.example.kotlin.dsl.model.Product.kt
package com.example.kotlin.dsl.model

import java.math.BigDecimal

/**
 * 商品数据传输对象
 * @param id 商品唯一标识
 * @param name 商品名称,非空
 * @param price 价格,使用BigDecimal避免浮点精度问题
 * @param tags 标签列表,可为空但默认为空列表
 */
data class Product(
    val id: String,
    val name: String,
    val price: BigDecimal,
    val tags: List<String> = emptyList() // 默认参数减少重载需求
)

// 使用示例
val product = Product(
    id = "SKU-001",
    name = "Kotlin Programming",
    price = BigDecimal("59.99")
)

// copy()实现不可变性更新,适用于MVVM状态流转
val discountedProduct = product.copy(
    price = product.price.multiply(BigDecimal("0.8"))
)

对比分析:

  • 空安全 :Kotlin强制标记可空类型,val name: String默认非空,编译器阻止null传入
  • 不可变性 :使用val声明只读属性,配合copy()实现函数式更新,避免并发修改风险
  • 自动生成 :compiler自动生成equals()、hashCode()、toString()、componentN()函数

2.2 密封类重构状态管理

kotlin 复制代码
// 文件路径: com.example.kotlin.dsl.state.OrderState.kt
package com.example.kotlin.dsl.state

import com.example.kotlin.dsl.model.Order

/**
 * 订单加载状态的密封类层次结构
 * 所有子类必须在此文件中定义,保证穷尽性检查
 */
sealed class OrderState {
    // object单例表示无数据状态
    object Idle : OrderState()
    object Loading : OrderState()
    
    // data class携带成功数据
    data class Success(val order: Order, val timestamp: Long = System.currentTimeMillis()) : OrderState()
    
    // data class携带错误信息,支持重试
    data class Error(
        val message: String, 
        val exception: Throwable? = null,
        val canRetry: Boolean = true
    ) : OrderState()
}

// ViewModel中的使用
class OrderViewModel(private val repository: OrderRepository) : ViewModel() {
    
    // StateFlow使用密封类作为状态载体,类型安全且响应式
    private val _uiState = MutableStateFlow<OrderState>(OrderState.Idle)
    val uiState: StateFlow<OrderState> = _uiState.asStateFlow()
    
    fun loadOrder(orderId: String) {
        viewModelScope.launch {
            _uiState.value = OrderState.Loading
            try {
                val order = repository.fetchOrder(orderId) // suspend函数
                _uiState.value = OrderState.Success(order)
            } catch (e: Exception) {
                _uiState.value = OrderState.Error(
                    message = "加载订单失败",
                    exception = e,
                    canRetry = e !is IllegalArgumentException // 参数错误不重试
                )
            }
        }
    }
}

核心优势:

  • 类型安全:编译器知道OrderState只有4种可能,switch-case无需default分支
  • 数据绑定:Success和Error携带不同数据,无需像Java那样维护多个LiveData
  • 穷尽检查:when表达式必须处理所有子类,新增状态后编译器强制要求补充处理

三、原理深挖:编译器魔法与JVM实现

3.1 数据类的字节码生成

Kotlin编译器对data class的处理并非简单的POJO。以Product类为例,编译后生成:

java 复制代码
// 反编译后的近似Java代码
public final class Product {
    private final String id;
    private final String name;
    private final BigDecimal price;
    private final List<String> tags;
    
    // 构造函数、getter...
    
    @NotNull
    public final String component1() { return this.id; } // 解构支持
    
    @NotNull
    public final Product copy(@NotNull String id, @NotNull String name, 
                             @NotNull BigDecimal price, @NotNull List<String> tags) {
        return new Product(id, name, price, tags);
    }
    
    @NotNull
    public String toString() { return "Product(id=" + this.id + ...)"; }
    
    public int hashCode() { /* 基于所有属性的哈希计算 */ }
    
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Product)) return false;
        // 逐个比较属性,对集合类型调用equals
        return Objects.equals(this.id, ((Product)other).id) && ...;
    }
}

性能考量:

  • 内存布局:与普通类相同,无额外字段开销
  • copy()开销:浅拷贝操作,仅复制引用,时间复杂度O(1);但需注意集合类型的深拷贝问题(见第五节)
  • 解构成本 :componentN()函数是是普通方法,但实现非常简单,通常会被JIT优化,实际开销可以忽略。

3.2 密封类的编译器策略

密封类的核心机制在于编译期类型封闭:

kotlin 复制代码
// 编译器在ModuleMetadata中记录所有子类信息
// 生成的字节码包含PermittedSubclasses属性(Java 17+)或Kotlin Metadata注解
public abstract class OrderState {
    private OrderState() {} // 私有构造函数阻止外部继承
    
    // 静态内部类/对象实现子类型
    public static final class Loading extends OrderState { ... }
}

when表达式穷尽性检查 : Kotlin编译器通过分析密封类的子类型封闭集合,在编译期验证when表达式是否覆盖所有分支。如果新增子类未处理,编译报错而非运行时异常。这比Java 17的switch pattern matching更严格,后者在跨模块边界时可能失去穷尽性保证。

与Java对比:

  • Java的sealed class需要显式permits子句,且子类必须同一模块
  • Kotlin 1.5+放宽限制,允许同一包内不同文件定义子类,但仍保持编译期封闭性
  • 性能无差异:密封类在JVM层面就是普通abstract class,判断分支时使用的是instanceof,JIT会优化为直接跳转

四、Android实战场景

场景1:RecyclerView多类型Adapter(密封类+数据类)

电商应用常见的混合列表:标题、商品卡片、分隔线、加载更多。

kotlin 复制代码
// com.example.kotlin.dsl.ui.FeedAdapter.kt
package com.example.kotlin.dsl.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlin.dsl.databinding.*
import com.example.kotlin.dsl.model.Product

/**
 * 列表项类型的密封类定义
 */
sealed class FeedItem {
    abstract val id: String // 用于DiffUtil比较
    
    data class Header(val title: String, val subtitle: String? = null) : FeedItem() {
        override val id: String = "header_$title"
    }
    
    data class ProductCard(val product: Product, val isNewArrival: Boolean = false) : FeedItem() {
        override val id: String = product.id
    }
    
    data class Divider(val heightDp: Int = 8) : FeedItem() {
        override val id: String = "divider_$heightDp"
    }
    
    object LoadingMore : FeedItem() {
        override val id: String = "loading_more"
    }
}

class FeedAdapter(
    private val onProductClick: (Product) -> Unit
) : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {

    // 根据item类型返回对应ViewHolder,编译器强制处理所有子类
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            TYPE_HEADER -> HeaderViewHolder(ItemHeaderBinding.inflate(inflater, parent, false))
            TYPE_PRODUCT -> ProductViewHolder(ItemProductBinding.inflate(inflater, parent, false), onProductClick)
            TYPE_DIVIDER -> DividerViewHolder(ItemDividerBinding.inflate(inflater, parent, false))
            TYPE_LOADING -> LoadingViewHolder(ItemLoadingBinding.inflate(inflater, parent, false))
            else -> throw IllegalArgumentException("Unknown view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // 使用类型安全的smart cast,无需手动强转
        when (val item = getItem(position)) {
            is FeedItem.Header -> (holder as HeaderViewHolder).bind(item)
            is FeedItem.ProductCard -> (holder as ProductViewHolder).bind(item)
            is FeedItem.Divider -> (holder as DividerViewHolder).bind(item)
            is FeedItem.LoadingMore -> { /* 无需绑定数据 */ }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is FeedItem.Header -> TYPE_HEADER
            is FeedItem.ProductCard -> TYPE_PRODUCT
            is FeedItem.Divider -> TYPE_DIVIDER
            is FeedItem.LoadingMore -> TYPE_LOADING
        }
    }

    companion object {
        private const val TYPE_HEADER = 0
        private const val TYPE_PRODUCT = 1
        private const val TYPE_DIVIDER = 2
        private const val TYPE_LOADING = 3
    }

    // ViewHolder实现类...
    class HeaderViewHolder(private val binding: ItemHeaderBinding) : 
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.Header) {
            binding.titleText.text = item.title
            binding.subtitleText.text = item.subtitle ?: ""
        }
    }
    
    class ProductViewHolder(
        private val binding: ItemProductBinding,
        private val onClick: (Product) -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.ProductCard) {
            binding.productName.text = item.product.name
            binding.priceText.text = "¥${item.product.price}"
            binding.root.setOnClickListener { onClick(item.product) }
        }
    }
    
    class DividerViewHolder(private val binding: ItemDividerBinding) : 
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.Divider) {
            binding.divider.layoutParams.height = 
                (item.heightDp * binding.root.context.resources.displayMetrics.density).toInt()
        }
    }
    
    class LoadingViewHolder(binding: ItemLoadingBinding) : 
        RecyclerView.ViewHolder(binding.root)

    class FeedDiffCallback : DiffUtil.ItemCallback<FeedItem>() {
        override fun areItemsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
            return oldItem == newItem // 利用data class自动生成的equals
        }
    }
}

最佳实践:

  • 使用object定义无数据状态(如LoadingMore),单例节省内存
  • abstract val id配合DiffUtil实现高效列表更新
  • 在onBindViewHolder中使用smart cast,Kotlin编译器自动推断类型,无需强制转换

场景2:ViewModel状态管理(密封类+StateFlow)

复杂表单页面的状态流转:

kotlin 复制代码
// com.example.kotlin.dsl.viewmodel.FormViewModel.kt
package com.example.kotlin.dsl.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class FormData(
    val username: String = "",
    val email: String = "",
    val age: Int? = null,
    val avatarUrl: String? = null
)

sealed class FormState {
    object Idle : FormState()
    data class Editing(val data: FormData, val isValid: Boolean = false) : FormState()
    data class Submitting(val data: FormData) : FormState()
    data class Success(val userId: String) : FormState()
    data class Error(val message: String, val fieldErrors: Map<String, String> = emptyMap()) : FormState()
}

class FormViewModel(private val api: UserApi) : ViewModel() {
    
    private val _state = MutableStateFlow<FormState>(FormState.Idle)
    val state: StateFlow<FormState> = _state.asStateFlow()
    
    // 暴露当前表单数据,方便UI回显
    val currentFormData: FormData
        get() = when (val s = _state.value) {
            is FormState.Editing -> s.data
            is FormState.Submitting -> s.data
            else -> FormData() // Idle/Success/Error状态下返回空表单
        }
    
    fun updateField(field: String, value: String) {
        val current = currentFormData
        val newData = when (field) {
            "username" -> current.copy(username = value)
            "email" -> current.copy(email = value)
            "age" -> current.copy(age = value.toIntOrNull())
            else -> current
        }
        
        // 实时验证逻辑
        val isValid = newData.username.isNotBlank() && 
                     newData.email.contains("@") &&
                     newData.age != null && newData.age > 0
        
        _state.value = FormState.Editing(newData, isValid)
    }
    
    fun submit() {
        val currentState = _state.value
        if (currentState !is FormState.Editing || !currentState.isValid) return
        
        viewModelScope.launch {
            _state.value = FormState.Submitting(currentState.data)
            try {
                val response = api.createUser(
                    username = currentState.data.username,
                    email = currentState.data.email,
                    age = currentState.data.age!!
                )
                _state.value = FormState.Success(response.userId)
            } catch (e: ValidationException) {
                _state.value = FormState.Error(
                    message = "表单验证失败",
                    fieldErrors = e.fieldErrors
                )
            } catch (e: NetworkException) {
                _state.value = FormState.Error(
                    message = "网络错误,请稍后重试",
                    fieldErrors = emptyMap()
                )
            }
        }
    }
    
    fun reset() {
        _state.value = FormState.Idle
    }
}

// UI层使用(Compose示例)
@Composable
fun FormScreen(viewModel: FormViewModel) {
    val state by viewModel.state.collectAsState()
    
    // 穷尽性处理确保所有状态都有UI对应
    when (val s = state) {
        is FormState.Idle -> IdleView(onStart = { viewModel.updateField("username", "") })
        is FormState.Editing -> FormEditingView(
            data = s.data,
            isValid = s.isValid,
            onFieldChange = viewModel::updateField,
            onSubmit = viewModel::submit
        )
        is FormState.Submitting -> LoadingView(data = s.data)
        is FormState.Success -> SuccessView(userId = s.userId, onReset = viewModel::reset)
        is FormState.Error -> ErrorView(
            message = s.message,
            fieldErrors = s.fieldErrors,
            onRetry = viewModel::submit,
            onReset = viewModel::reset
        )
    }
}

注意事项:

  • 状态转换逻辑集中管理,避免UI层直接修改状态
  • Submitting携带data用于展示"正在提交:xxx"的友好提示
  • 使用StateFlow确保配置变更(如旋转屏幕)时状态不丢失

场景3:Repository层网络状态封装(密封类+数据类)

统一处理网络请求的Loading/Error/Success状态:

kotlin 复制代码
// com.example.kotlin.dsl.repository.Result.kt
package com.example.kotlin.dsl.repository

/**
 * 通用网络结果封装
 * @param T 成功时返回的数据类型
 */
sealed class Result<out T> {
    data class Success<T>(val data: T, val fromCache: Boolean = false) : Result<T>()
    data class Error(val exception: Throwable, val code: Int? = null) : Result<Nothing>()
    object Loading : Result<Nothing>()
    
    // 扩展函数:方便处理结果
    fun getOrNull(): T? = (this as? Success<T>)?.data
    fun exceptionOrNull(): Throwable? = (this as? Error)?.exception
    val isSuccess: Boolean get() = this is Success
    val isLoading: Boolean get() = this is Loading
}

// Repository实现
class ProductRepository(private val api: ProductApi, private val dao: ProductDao) {
    
    fun getProducts(categoryId: String): Flow<Result<List<Product>>> = flow {
        emit(Result.Loading)
        
        try {
            // 先尝试缓存
            val cached = dao.getByCategory(categoryId)
            if (cached.isNotEmpty()) {
                emit(Result.Success(cached, fromCache = true))
            }
            
            // 网络请求
            val remote = api.fetchProducts(categoryId)
            dao.insertAll(remote) // 更新缓存
            emit(Result.Success(remote, fromCache = false))
        } catch (e: Exception) {
            emit(Result.Error(e, code = (e as? HttpException)?.code()))
        }
    }
    
    // 使用示例:在ViewModel中收集
    fun loadProducts(categoryId: String) {
        viewModelScope.launch {
            repository.getProducts(categoryId)
                .onStart { /* 可选的额外初始化 */ }
                .collect { result ->
                    when (result) {
                        is Result.Loading -> _uiState.value = UiState.Loading
                        is Result.Success -> {
                            val message = if (result.fromCache) "已同步离线数据" else "数据已更新"
                            _uiState.value = UiState.Success(result.data, message)
                        }
                        is Result.Error -> {
                            val msg = when (result.code) {
                                404 -> "分类不存在"
                                500 -> "服务器错误"
                                else -> result.exception.message ?: "未知错误"
                            }
                            _uiState.value = UiState.Error(msg, canRetry = result.code != 404)
                        }
                    }
                }
        }
    }
}

最佳实践:

  • 使用泛型密封类Result<T>统一所有网络请求返回类型
  • Nothing类型用于无数据的Loading/Error状态,确保类型安全
  • 扩展函数封装常用操作,避免重复的类型判断

五、踩坑指南:反模式与正确实践

坑点1:Data Class的浅拷贝陷阱(mutable collection引用共享)

错误代码:

kotlin 复制代码
data class UserProfile(
    val userId: String,
    val tags: MutableList<String> // 可变集合作为属性
)

val profile = UserProfile("U001", mutableListOf("VIP", "Active"))
val backup = profile.copy() // 浅拷贝,tags引用相同

backup.tags.add("Suspended") // 意外修改了profile的tags!
println(profile.tags) // [VIP, Active, Suspended] - 原始数据被污染

正确做法:

kotlin 复制代码
// 方案1:使用不可变集合(推荐)
data class UserProfile(
    val userId: String,
    val tags: List<String> = emptyList() // 声明为List(只读接口)
)

// 需要修改时创建新集合
val newProfile = profile.copy(
    tags = profile.tags + "Suspended" // 返回新列表,原列表不变
)

// 方案2:防御性拷贝(必须暴露可变集合时)
data class UserProfile private constructor(
    val userId: String,
    private val _tags: MutableList<String>
) {
    val tags: List<String> get() = _tags.toList() // 每次返回副本
    
    companion object {
        fun create(userId: String, tags: List<String>) = 
            UserProfile(userId, tags.toMutableList())
    }
    
    // 自定义深拷贝方法
    fun deepCopy(userId: String = this.userId, tags: List<String> = this.tags.toList()) = 
        create(userId, tags)
}

核心原则 :数据类的设计哲学是不可变性 。一旦在primary constructor中放入可变对象(MutableList、Array等),copy()和equals()的行为将违背预期。

坑点2:Sealed Class跨模块使用的限制

错误认知:认为密封类可以在不同模块间自由扩展。

实际情况:

kotlin 复制代码
// Module A (core模块)
sealed class NetworkState

// Module B (feature模块) - 编译错误!
data class CustomError : NetworkState() // 不允许,密封类子类必须在同一包内

解决方案:

kotlin 复制代码
// 使用抽象类+内部密封类组合,允许部分开放
// Module A
abstract class NetworkState {
    abstract val timestamp: Long
    
    // 核心状态密封
    sealed class Core : NetworkState() {
        data class Loading(override val timestamp: Long) : Core()
        data class Error(val message: String, override val timestamp: Long) : Core()
    }
    
    // 开放给子模块扩展的接口
    interface Extendable
}

// Module B - 可以扩展抽象类,但失去when穷尽性检查
data class CustomState(val data: String, override val timestamp: Long) : 
    NetworkState(), NetworkState.Extendable

建议 :若需跨模块扩展,考虑使用sealed interface(Kotlin 1.5+)配合编译插件,或放弃穷尽性检查改用抽象类。

坑点3:过度使用Data Class作为领域模型

错误场景:

kotlin 复制代码
// 将包含业务逻辑的实体强行定义为data class
data class Order(
    val id: String,
    val items: List<OrderItem>,
    val status: OrderStatus
) {
    // 严重错误:在data class中放业务逻辑
    fun calculateTotal(): BigDecimal = 
        items.fold(BigDecimal.ZERO) { sum, item -> 
            sum + item.price * item.quantity.toBigDecimal()
        }
    
    fun applyDiscount(rule: DiscountRule): Order = 
        copy(items = items.map { rule.apply(it) }) // 业务逻辑污染数据类
}

正确分层:

kotlin 复制代码
// 纯数据类,仅承载状态
data class Order(
    val id: String,
    val items: List<OrderItem>,
    val status: OrderStatus,
    val totalAmount: BigDecimal, // 计算结果作为字段存储
    val discountApplied: BigDecimal = BigDecimal.ZERO
)

// 领域服务处理业务逻辑
class OrderService {
    fun calculateTotal(order: Order): BigDecimal = 
        order.items.sumOf { it.price * it.quantity.toBigDecimal() }
    
    fun applyDiscount(order: Order, rule: DiscountRule): Order {
        val discount = rule.calculate(order)
        return order.copy(
            totalAmount = order.totalAmount - discount,
            discountApplied = discount
        )
    }
}

设计准则:

  • Data Class:DTO、VO、状态载体、配置项(无行为,只有数据)
  • 普通Class/Sealed Class:包含业务逻辑的领域实体、需要继承层次的状态机
  • 数据类不应知晓存储逻辑(Room的@Entity除外,那是框架侵入)

总结:Kotlin的数据类与密封类通过编译期代码生成和类型封闭,将Android业务建模从"防御式编程"(防御null、防御类型错误、防御忘记处理分支)转变为"声明式编程"。掌握其字节码原理与不可变性设计哲学,方能避免浅拷贝陷阱,构建出类型安全、高可维护的Android架构。

相关推荐
tongyiixiaohuang4 小时前
WMS已发货状态反写金蝶云星空:单策略实战教程
android
好的佩奇5 小时前
Dart 之类修饰符
android·flutter·dart
alexhilton6 小时前
用Konsist与Lint强制执行设计系统契约
android·kotlin·android jetpack
唐青枫8 小时前
Kotlin Delegation 委托详解
kotlin
北京自在科技10 小时前
谷歌安卓 Gemini 迎来界面改版,Find Hub 接入 Gemini 支持物品记忆查找
android·安卓
YB137512 小时前
Jetpack Compose 动画Modifier.animateItem()
android
2401_8858850413 小时前
巴林语音通知接口如何对接?系统告警语音API接入教程
android·前端·网络·python·语音识别
收纳过往13 小时前
数据库加索引的时机:慢查询日志说话
android·开发语言·数据库·python·低代码·rxjava
狂热开发者14 小时前
车载中控要给鸿蒙用户,能不能和 Android 共用一个二维码?
android·华为·harmonyos
.Bryce.15 小时前
T2-LLM评测不等于Agent评测
android·java·人工智能·后端·gpt·agent·1024程序员节