Kotlin中Dagger @IntoMap与@ClassKey使用困惑及类型约束问题
问题描述
在Kotlin中使用Dagger实现Set集合创建与Map注入时,遇到以下三个问题:
@ClassKey注解的Kotlin使用疑问
@ClassKey的Java定义如下:@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) @Retention(RUNTIME) @MapKey public @interface ClassKey { Class<?> value(); }它的参数类型为
java.lang.Class,但在Kotlin中使用@ClassKey(ErikTenHag::class)可正常运行,使用@ClassKey(ErikTenHag::class.java)却触发报错。Map键类型为KClass时的注入失败
注入点定义为以下代码时:@JvmSuppressWildcards data class FootballLeague @Inject constructor(val teamMap: Map<@JvmSuppressWildcards KClass<*>, Set<Player>>) {出现编译错误:
[Dagger/MissingBinding] java.util.Map<kotlin.reflect.KClass<?>,java.util.Set
> cannot be provided without an @Provides-annotated method.
将注入点的键类型改为Class<*>后,编译正常且功能符合预期:@JvmSuppressWildcards data class FootballLeague @Inject constructor(val teamMap: Map<@JvmSuppressWildcards Class<*>, Set<Player>>) {约束Map键类型为具体接口/类的失败尝试
不想使用通配符*,尝试给Map键指定Manager子类约束:@JvmSuppressWildcards data class FootballLeague<M: Manager> @Inject constructor(val teamMap: Map<@JvmSuppressWildcards Class<M>, Set<Player>>) {但编译报错:
error: [Dagger/MissingBinding] java.util.Map<java.lang.Class
,java.util.Set > cannot be provided without an @Provides-annotated method.
想确认是否必须使用通配符,或存在其他约束类型的实现方案。
完整代码
@Module @InstallIn(SingletonComponent::class) class FootballTeamModule { @Provides @ElementsIntoSet @Named("LiverpoolPlayers") fun providesLiverpoolPlayers(mohamedSalah: MohamedSalah, virgilVanDyke: VirgilVanDyke): Set<Player> { return HashSet<Player>(listOf(mohamedSalah, virgilVanDyke)) } @Provides @ElementsIntoSet @Named("ManchesterPlayers") fun providesManchesterPlayers(harryMaguire: HarryMaguire, masonMount: MasonMount, casimiro: CarlosCasimiro): Set<Player> { return HashSet<Player>(listOf(harryMaguire, masonMount, casimiro)) } @Provides @IntoMap @ClassKey(JurgenKlopp::class) //@StringKey("JurgenKlopp") fun providesLiverpool(@Named("LiverpoolPlayers") players: Set<@JvmSuppressWildcards Player>): Set<Player> { return players } @Provides @IntoMap @ClassKey(ErikTenHag::class) //@StringKey("ErikTenHag") fun providesManchester(@Named("ManchesterPlayers") players: Set<@JvmSuppressWildcards Player>): Set<Player> { return players } } interface Player { fun name(): String fun wages(): Double fun position(): String } interface Manager { fun name(): String fun nationality(): String } class MohamedSalah @Inject constructor() : Player { override fun name(): String = this::class.java.simpleName override fun wages(): Double = 18200000.00 override fun position(): String = "Forward" override fun toString(): String { return "${name()} - ${wages()} - ${position()}" } } class VirgilVanDyke @Inject constructor() : Player { override fun name(): String = this::class.java.simpleName override fun wages(): Double = 11440000.00 override fun position(): String = "Defender" override fun toString(): String { return "${name()} - ${wages()} - ${position()}" } } class JurgenKlopp @Inject constructor() : Manager { override fun name(): String = this::class.java.simpleName override fun nationality(): String = "German" } class ErikTenHag @Inject constructor() : Manager { override fun name(): String = this::class.java.simpleName override fun nationality(): String = "Netherlands" } class CarlosCasimiro @Inject constructor() : Player { override fun name(): String = this::class.java.simpleName override fun wages(): Double = 118200000.00 override fun position(): String = "Midfield" override fun toString(): String { return "${name()} - ${wages()} - ${position()}" } } class MasonMount @Inject constructor() : Player { override fun name(): String = this::class.java.simpleName override fun wages(): Double = 13000000.00 override fun position(): String = "Midfield" override fun toString(): String { return "${name()} - ${wages()} - ${position()}" } } class HarryMaguire @Inject constructor() : Player { override fun name(): String = this::class.java.simpleName override fun wages(): Double = 1.50 override fun position(): String = "Defender" override fun toString(): String { return "${name()} - ${wages()} - ${position()}" } } @JvmSuppressWildcards data class FootballLeague @Inject constructor(val teamMap: Map<@JvmSuppressWildcards Class<*>, Set<Player>>) { fun getFootballLeagueInstance() { teamMap.entries.forEach { println("xxooxx ${it.key} to ${it.value}") } } }
问题解答
1. @ClassKey中::class与::class.java的差异
这是Kotlin与Java互操作的语法糖导致的:当Java注解接收Class类型参数时,Kotlin会自动将KClass(即::class的返回值)隐式转换为对应的java.lang.Class对象。
直接使用::class.java报错,大概率是因为Dagger注解处理器的类型匹配严格性——虽然Class<ErikTenHag>属于Class<?>的子类型,但部分旧版本Dagger可能对显式传入的Class对象做更严格的泛型检查。本质上两者最终传递的是同一个Class实例,推荐使用::class的写法,符合Kotlin的语法习惯。
2. KClass作为Map键注入失败的原因
Dagger的@IntoMap与@ClassKey是基于Java的Class类型实现绑定的。你在Module中用@ClassKey(JurgenKlopp::class)注入的是Class<JurgenKlopp>作为键的条目,而注入点要求的是KClass<*>类型的键——这是两种完全不同的反射类型(KClass是Kotlin专属,Class是Java的),Dagger无法找到匹配的绑定,因此必须统一使用Class类型作为键。
3. 约束Map键类型的替代方案
不需要使用通配符*,可以通过以下两种方案实现类型约束:
方案一:使用out通配符缩小类型范围
修改注入点的Map键类型为Class<out Manager>,表示键必须是Manager子类的Class对象:
@JvmSuppressWildcards data class FootballLeague @Inject constructor( val teamMap: Map<@JvmSuppressWildcards Class<out Manager>, Set<Player>> ) { fun getFootballLeagueInstance() { teamMap.entries.forEach { println("xxooxx ${it.key.simpleName} -> ${it.value.joinToString()}") } } }
这种写法既约束了键的类型范围,又能让Dagger正确匹配Module中JurgenKlopp::class和ErikTenHag::class的绑定(两者都是Manager的子类)。
方案二:自定义限定MapKey注解
如果需要更严格的类型约束,可以自定义一个仅接受Manager子类的MapKey注解:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) @Retention(RUNTIME) @MapKey public @interface ManagerClassKey { Class<? extends Manager> value(); }
然后在Module中替换原有的@ClassKey:
@Provides @IntoMap @ManagerClassKey(JurgenKlopp::class) fun providesLiverpool(@Named("LiverpoolPlayers") players: Set<@JvmSuppressWildcards Player>): Set<Player> { return players } @Provides @IntoMap @ManagerClassKey(ErikTenHag::class) fun providesManchester(@Named("ManchesterPlayers") players: Set<@JvmSuppressWildcards Player>): Set<Player> { return players }
注入点同样使用Class<out Manager>作为键类型即可,既保证了类型安全,又能正常注入。
为什么泛型类的方式不行?
Dagger在编译时处理依赖注入,泛型类型会被擦除:FootballLeague<M: Manager>会被擦除为FootballLeague<Manager>,Dagger会寻找Map<Class<Manager>, Set<Player>>的绑定,但你Module中提供的是Map<Class<JurgenKlopp>, ...>和Map<Class<ErikTenHag>, ...>,这些不是Map<Class<Manager>, ...>的子类型,因此无法匹配。
内容的提问来源于stack exchange,提问作者Hector

