代码之家  ›  专栏  ›  技术社区  ›  Andrey Busik

将参数传递给具有多个上界的泛型函数

  •  6
  • Andrey Busik  · 技术社区  · 7 年前

    无法将参数传递给 Combiner().combine() 功能。

    Android工作室无法识别 arg 延伸 Foo 和工具 Bar . 我做错什么了?

    abstract class Foo {
        val f: Int = 1
    }
    
    interface Bar {
        val b: String get() = "a"
    }
    
    class Combiner {
        fun <T> combine(arg: T): Pair<Int, String> where T : Foo, T : Bar {
            return arg.f to arg.b
        }
    }
    
    class Program {
        fun main() {
            val list: List<Foo> = arrayListOf()
            list.forEach {
                if (it is Bar) {
                    Combiner().combine(it) //inferred type Any is not a subtype of Foo
                }
            }
        }
    }
    

    这就是Java的工作原理:

    public static class Program {
        public static void main() {
            List<Foo> list = new ArrayList<>();
            for (Foo item : list) {
                if (item instanceof Bar) {
                    new Combiner().combine((Foo & Bar) item);
                }
            }
        }
    }
    

    为Kotlin创建了错误报告: https://youtrack.jetbrains.com/issue/KT-25942

    2 回复  |  直到 7 年前
        1
  •  3
  •   EpicPandaForce Jigar Joshi    7 年前

    如果这有什么帮助的话,显然如果你用Java写同样的东西:

    abstract class Foo {
        public int getF() {
            return 1;
        }
    }
    
    interface Bar {
        default String getB() {
            return "a";
        }
    }
    
    static class Combiner {
        public <T extends Foo & Bar> Pair<Integer, String> combine(T arg) {
            return Pair.create(arg.getF(), arg.getB()); 
        }
    }
    
    public static class Program {
        public static void main() {
            List<Foo> list = new ArrayList<>();
            list.forEach(foo -> {
                if(foo instanceof Bar) {
                    new Combiner().combine(foo);
                }
            });
        }
    }
    

    那么它将无法工作,因为以下消息:

    原因:没有任何类型变量的实例存在,所以FO符合BAR 推理变量T具有不兼容的界限: 下限:Foo 上界:Foo,Bar

    现在如果你加上 cast Bar :

            list.forEach(foo -> {
                if(foo instanceof Bar) {
                    new Combiner().combine((Bar)foo);
                }
            });
    

    问题显而易见: (Bar)foo 现在是 酒吧 ,而不是 Foo .


    所以你需要知道一个确切的类型 酒吧 为了投球。

    如果是这样,那么 能够 工作-事实上,在Java中,它实际上编译:

    public static <T extends Foo & Bar> T toBar(Foo foo) {
        //noinspection unchecked
        return (T)foo;
    }
    
    public static class Program {
        public static void main() {
            List<Foo> list = new ArrayList<>();
            list.forEach(foo -> {
                if(foo instanceof Bar) {
                    new Combiner().combine(toBar(foo));
                }
    

    实际上,下面的测试 成功 :

    public static class Pair<S, T> {
        public Pair(S first, T second) {
            this.first = first;
            this.second = second;
        }
    
        S first;
        T second;
    
        public static <S, T> Pair<S, T> create(S first, T second) {
            return new Pair<>(first, second);
        }
    }
    
    public static <T extends Foo & Bar> T toBar(Foo foo) {
        //noinspection unchecked
        return (T)foo;
    }
    
    public class Blah extends Foo implements Bar {
    }
    
    @Test
    public void castSucceeds() {
        Blah blah = new Blah();
        List<Foo> list = new ArrayList<>();
        list.add(blah);
    
        list.forEach(foo -> {
            if(foo instanceof Bar) {
                Pair<Integer, String> pair = new Combiner().combine(toBar(foo));
                assertThat(pair.first).isEqualTo(1);
                assertThat(pair.second).isEqualTo("a");
            }
        });
    }
    

    也就是说,在Kotlin,理论上应该是这样的:

    class Program {
        fun main() {
            val list: List<Foo> = arrayListOf()
            list.forEach {
                if (it is Bar) {
                    @Suppress("UNCHECKED_CAST")
                    fun <T> Foo.castToBar(): T where T: Foo, T: Bar = this as T
    
                    Combiner().combine(it.castToBar()) // <-- magic?
                }
            }
        }
    }
    

    但它不起作用,因为它说:

    类型推断失败:没有足够的信息来推断参数T。请显式指定它。

    所以在科特林,我能做的就是:

    class Blah: Foo(), Bar {
    }
    
    Combiner().combine(it.castToBar<Blah>())
    

    这显然是不能保证的,除非我们知道它的特定子类型是Foo和Bar的子类。


    所以我似乎找不到方法让Kotlin将一个类转换为它自己的类型,因此“相信我”,它可以安全地转换为 T 它本身就是Foo和Bar的一个子类。

    但让Kotlin相信通过Java可以做到:

    import kotlin.Pair;
    
    public class ForceCombiner {
        private ForceCombiner() {
        }
    
        private static <T extends Foo & Bar> Pair<Integer, String> actuallyCombine(Bar bar) {
            T t = (T)bar;
            return new Combiner().combine(t);
        }
    
        public static Pair<Integer, String> combine(Bar bar) {
            return actuallyCombine(bar);
        }
    }
    

    以及

    class Program {
        fun main() {
            val list: List<Foo> = arrayListOf()
            list.forEach {
                if (it is Bar) {
                    val pair = ForceCombiner.combine(it) // <-- should work
    

    除了 ForceCombiner 现在只有当我们使用 @JvmDefault 在Kotlin接口上

    interface Bar {
        @JvmDefault
        val b: String get() = "a"
    }
    

    它现在说:

    // Inheritance from an interface with `@JvmDefault` members is only allowed with -Xjvm-default option
    class Blah: Foo(), Bar { 
    }
    

    所以我没试过 -Xjvm-default 选择,但是 it could work ? 见 here how you can do that .

    • 在-Xjvm default=enable的情况下,只为每个@JvmDefault方法生成接口中的默认方法。在这种模式下,用jjvMeRead注释现有的方法可以打破二进制兼容性,因为它将有效地从DEFAUFIMPPLS类中移除该方法。
    • 在-Xjvm default=compatibility的情况下,除了默认接口方法之外,还会在DefaultImpls类中生成一个compatibility访问器,该访问器通过一个合成访问器调用默认接口方法。在这种模式下,用@注释现有的方法是二进制兼容的,但会导致字节码中使用更多的方法。

    也, @JVM默认 要求 target 1.8 ,但安卓的减糖功能 应该 现在处理默认接口。

        2
  •  1
  •   Alexey Soshin    7 年前

    错误消息有些模糊。让我们来帮助Kotlin编译器:

    fun main(vararg args: String) {
         val list: List<Foo> = arrayListOf()
         list.forEach {
              val bar = it as Bar
              val c = Combiner().combine(bar) // inferred type Bar is not a subtype of Foo
         }
    }
    

    聪明的演员所做的只是-他们在演。一旦你投了 Foo 进入之内 Bar ,不能保证它是 不再。