You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Kotlin契约强制Builder初始化所有必填字段?求更优实现

Great question! The two implementations you’ve shared each have clear tradeoffs, but we can combine their strengths and fix most of their pain points with a stage-based contract builder that leverages Kotlin’s contracts, inline functions, and constrained generics. This approach addresses private property access, DSL support, duplicate assignment prevention, and generic bloat while keeping compile-time safety.

Improved Implementation: Stage-Based Contract Builder

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

// Define build stage markers as sealed interfaces for strict hierarchy
sealed interface PersonBuildStage
interface Unnamed : PersonBuildStage
interface Named : PersonBuildStage
interface NamedAndAged : PersonBuildStage

@OptIn(ExperimentalContracts::class)
class PersonBuilder<out T : PersonBuildStage> private constructor(
    private var _name: String? = null,
    private var _age: Int? = null
) {
    // Start in the Unnamed stage by default
    companion object {
        operator fun invoke(): PersonBuilder<Unnamed> = PersonBuilder()

        // DSL support with inline function and contract for type inference
        @OptIn(ExperimentalContracts::class)
        inline fun build(block: PersonBuilder<Unnamed>.() -> PersonBuilder<NamedAndAged>): Person {
            contract {
                callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
            }
            return invoke().run(block).build()
        }
    }

    // Only callable from Unnamed stage; transitions to Named stage
    fun name(name: String): PersonBuilder<Named> {
        contract {
            returns() implies (this@name is PersonBuilder<Named>)
        }
        require(_name == null) { "Name cannot be set more than once" }
        _name = name
        @Suppress("UNCHECKED_CAST") // Safe due to stage transition logic
        return this as PersonBuilder<Named>
    }

    // Only callable from Named stage; transitions to completed stage
    fun age(age: Int): PersonBuilder<NamedAndAged> {
        contract {
            returns() implies (this@age is PersonBuilder<NamedAndAged>)
        }
        require(_age == null) { "Age cannot be set more than once" }
        _age = age
        @Suppress("UNCHECKED_CAST")
        return this as PersonBuilder<NamedAndAged>
    }

    // Only callable from the fully completed stage
    fun build(): Person {
        contract {
            returns() implies (this@build is PersonBuilder<NamedAndAged>)
        }
        return Person(
            name = _name ?: error("Name must be set before building"),
            age = _age ?: error("Age must be set before building")
        )
    }
}

data class Person(val name: String, val age: Int)

How This Fixes Your Original Pain Points

Let’s break down how this implementation addresses the flaws in your two approaches:

  • Private properties: _name and _age are now private, eliminating external direct access (no more public backing fields)
  • DSL compatibility: The inline build function with a contract enables type-safe, receiver-based DSLs—no more issues with this type inference
  • No duplicate assignments: Compile-time stage constraints prevent re-calling name/age on already set stages, and runtime require adds an extra safety net
  • Controlled generic bloat: Stages use a sealed interface hierarchy instead of separate generic parameters. Adding a new field (like email) only requires a new stage interface (e.g., NamedAndAgedAndEmailed) instead of adding another generic type
  • Compile-time safety: Generic stage constraints + contracts ensure build can only be called after all required fields are set
  • Minimal boilerplate: No messy where clauses—stage constraints are handled directly via generic type bounds

Minor Remaining Tradeoffs

  • We still use @Suppress("UNCHECKED_CAST") for stage transitions, but this is a standard pattern for this kind of builder and doesn’t compromise safety (the contract and runtime checks enforce valid state)
  • Runtime require checks are redundant for most cases (since the compiler blocks invalid calls) but serve as a safety net for edge cases (like reflection-based access)

Usage Examples

Chained Calls

// Valid: All required fields set before build
val bob = PersonBuilder().name("Bob").age(21).build()

// Compile error: Cannot call build without setting age
// PersonBuilder().name("Bob").build()

// Compile error: Cannot re-set name
// PersonBuilder().name("Bob").name("Ann")

DSL Calls

// Valid DSL usage
val alice = PersonBuilder.build {
    name("Alice")
    age(25)
}

// Compile error: Missing age field
// val invalidPerson = PersonBuilder.build {
//     name("Invalid")
// }

Passing Partial Builders

// Function only accepts builders that have already set a name
fun finalizeNamedBuilder(builder: PersonBuilder<Named>) {
    builder.age(30).build()
}

val namedBuilder = PersonBuilder().name("Charlie")
finalizeNamedBuilder(namedBuilder)

// Compile error: Cannot pass an Unnamed builder
// finalizeNamedBuilder(PersonBuilder())

Bonus: Kotlin 1.7+ Context Receivers

If you’re using Kotlin 1.7 or later, you can simplify stage constraints even further with context receivers, making the code more readable and eliminating explicit generic bounds in function signatures:

@OptIn(ExperimentalContracts::class)
context(Unnamed)
fun PersonBuilder<*>.name(name: String): PersonBuilder<Named> {
    contract { returns() implies (this@name is PersonBuilder<Named>) }
    require(_name == null) { "Name already set" }
    _name = name
    @Suppress("UNCHECKED_CAST")
    return this as PersonBuilder<Named>
}

context(Named)
fun PersonBuilder<*>.age(age: Int): PersonBuilder<NamedAndAged> {
    contract { returns() implies (this@age is PersonBuilder<NamedAndAged>) }
    require(_age == null) { "Age already set" }
    _age = age
    @Suppress("UNCHECKED_CAST")
    return this as PersonBuilder<NamedAndAged>
}

context(NamedAndAged)
fun PersonBuilder<*>.build(): Person {
    return Person(_name!!, _age!!)
}

Context receivers make the stage requirements for each function explicit at the declaration site, leading to cleaner, more intuitive code.


内容的提问来源于stack exchange,提问作者IlyaMuravjov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:15:16