动态类型检查匹配类型参数

huangapple go评论79阅读模式
英文:

Dynamic type check matching type parameters

问题

我希望实现一个具有以下签名的方法
```Java
<T> void function(Class<T> clazz, Consumer<T> consumer)

这个函数接受一个Object obj(在其他地方提供),使用clazz.cast(obj)将其转换为T,然后调用consumer。 (基本上,它是consumer的保护程序。)

这个方法大致可以正常工作,我可以编写

Consumer<String> c = ...;
function(String.class, c);

不幸的是,以下代码不起作用:

Consumer<List<String>> c = ...;
function(List.class, c);

这是因为List.class具有类型Class<List>,而不是Class<List<String>>

我可以将c的类型更改为Consumer<List>,但这将与我的其他代码不兼容(而且原始类型也不好)。

是否有一种方式可以(a)以类型检查方式调用function或者(b)以使这种模式起作用的方式更改function的类型签名?

注意:

  • 我希望尽可能保留编译时和运行时的类型安全性。因此,function(Class<?> clazz, Consumer<T> consumer)是不可接受的,因为用户可以轻松提供错误的clazz。而且,以function((Class<List<String>>)List.class, c)的方式调用也是不可接受的,因为我可能会意外地将其强制转换为function((Class<Integer>)List.class, c),而这种错误的强制转换甚至在运行时都不会被捕获。如果有一种函数允许将Class<C<T>>强制转换为Class<C<U>>,但不能强制转换为Class<D>,那将很酷,但我看不到这一点,除非使用高阶类型。

  • 我知道类型转换将无法在运行时检查我们是否有一个List<String>而不是List<Integer>。这是可以接受的。(嗯,不是,但我假设在JVM语言中这是我们能期望的最好情况。)

  • 一切相同的情况下,我更喜欢使function的调用简单(可能使function更复杂),因为function在我的库中定义,由该库的用户使用。

  • 对于好奇的人,我遇到这个问题的实际代码在这里Instance就是function


<details>
<summary>英文:</summary>

I wish to implement a method with the following signature:
```Java
&lt;T&gt; void function(Class&lt;T&gt; clazz, Consumer&lt;T&gt; consumer)

This function takes an Object obj (supplied elsewhere), uses clazz.cast(obj) to cast it to T, and then invokes consumer with it. (Basically, it is a guard for consumer.)

This works more or less fine, I can write

Consumer&lt;String&gt; c = ...;
function(String.class, c);

Unfortunately, the following code does not work:

Consumer&lt;List&lt;String&gt;&gt; c = ...;
function(List.class, c);

This is because List.class has type Class&lt;List&gt;, not class Class&lt;List&lt;String&gt;&gt;.
I can change the type of c to Consumer&lt;List&gt;, but that would be incompatible with my code in other places (and raw types are not nice anyway).

Is there some way to (a) invoke function in a way that typechecks or (b) change the type signature of function in a way that makes this pattern work?

Notes:

  • I wish to retain as much compile and runtime type safety as possible. So, function(Class&lt;?&gt; clazz, Consumer&lt;T&gt; consumer) would not be acceptable because the user can easily provide the wrong clazz. And invoking as function((Class&lt;List&lt;String&gt;&gt;)List.class, c) is also not acceptable because I could accidentally cast it as function((Class&lt;Integer&gt;)List.class, c) and
    this wrong cast would not even be caught at runtime. If there was a function that allows to cast Class&lt;C&lt;T&gt;&gt; to Class&lt;C&lt;U&gt;&gt; but not to Class&lt;D&gt;, that would be cool, but I don't see that without higher-order types.

  • I am aware that the type casts will not be able to check at runtime that we have a List&lt;String&gt; and not a List&lt;Integer&gt;. That's OK. (Well, it's not, but I assume it's the best
    we can expect in a JVM language.)

  • All being the same, I prefer solutions that make the invocation of function simple (possible making function more complex) because function is defined in my library and used by the user of that library.

  • For the curious, the actual code where I have this problem is here, Instance is function.

答案1

得分: 2

这只使用 Class&lt;T&gt; 参数是无法实现你想要的功能的。你需要一个超类型标记来代替。

正如链接的文章中所解释的,以下是你可能实现这种功能的方式:

public abstract class TypeToken&lt;T&gt; {
 
    private final Type type;
 
    public TypeToken() {
        Type superclass = getClass().getGenericSuperclass();
        type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
 
    public Type getType() {
        return type;
    }
}

在Java中,你无法获取关于实例的泛型类型变量的信息,但你可以获取关于超类型的泛型类型变量的一些信息。请注意,TypeToken 类是抽象的。以下是你在示例中应该如何使用它:

&lt;T&gt; void function(TypeToken&lt;T&gt; token, Consumer&lt;T&gt; consumer)

Consumer&lt;List&lt;String&gt;&gt; c = ...;

function(new TypeToken&lt;List&lt;String&gt;&gt;() { }, c);

这意味着你将一个 TypeToken 的子类传递给了你的函数。

最后,在 function 内部,你可以提取一个 Class 实例,并进行类型转换,如下所示:

Class&lt;T&gt; clazz;
if (type instanceof ParameterizedType)
  clazz = (Class&lt;T&gt;) ((ParameterizedType) type).getRawType();
else if (typeU instanceof Class)
  clazz = (Class&lt;T&gt;) type;
else
  throw new RuntimeException("Error handling omitted");
T t = clazz.cast(obj);
英文:

It's not possible to do what you want with just a Class&lt;T&gt; argument. You need a super type token instead.

As explained in the linked article, this is how you might implement such functionality:

public abstract class TypeToken&lt;T&gt; {
 
    private final Type type;
 
    public TypeToken() {
        Type superclass = getClass().getGenericSuperclass();
        type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
 
    public Type getType() {
        return type;
    }
}

In Java, you cannot grab info about generic type variables of instances, however you can get some info about the generic type variables of super types. Note that the TypeToken class is abstract. This is how you should use it in your example:

&lt;T&gt; void function(TypeToken&lt;T&gt; token, Consumer&lt;T&gt; consumer)

Consumer&lt;List&lt;String&gt;&gt; c = ...;

function(new TypeToken&lt;List&lt;String&gt;&gt;() { }, c);

This means that you are passing a subclass of TypeToken to your function.

Finally, inside function, you can extract a Class instance and type cast an object as follows:

Class&lt;T&gt; clazz;
if (type instanceof ParameterizedType)
  clazz = (Class&lt;T&gt;) ((ParameterizedType) type).getRawType();
else if (typeU instanceof Class)
  clazz = (Class&lt;T&gt;) type;
else
  throw new RuntimeException(&quot;Error handling omitted&quot;);
T t = clazz.cast(obj);

答案2

得分: 0

我认为不可能达到你所追求的类型安全程度。

让我们区分编译时和运行时。

编译时

在编译时,编译器会检查 clazz 的类型是否与 consumer 的类型参数 T 匹配。但是对于类型 T 本身具有泛型类型参数的情况,比如 List<String>,在Java中没有 List<String>.class 的语法。所以,在这种情况下,你最好可以做的就是将 List.class 强制转换为 Class<List<String>>

在你的 function 中操作的对象 obj 在方法的签名中不可见,所以在编译时没有机会检查其类型是否与 consumer 匹配。

你完全可以调用 function(String.class, stringConsumer) 并传递一个 Integer 对象。编译器不会检测到这个错误。

运行时

在运行时,不存在泛型信息。这个概念被称为 "类型擦除"。因此,在编译时看起来是 List<String>,在运行时只是一个 List

就像在编译时一样,objfunction 调用时不可见,所以你将类型检查移到了运行时,使用 Class.cast() 方法。

如果在编译时,你的 objList<String>List<Integer> 或者 List<Whatever>,在运行时它只是一个没有参数的原始 List,你要检查的类也只是原始的 List.class,没有任何类型参数。cast() 方法将接受所有这些不同的列表。

因此,在运行时,没有办法拒绝一个 List<Integer> 进入一个期望 List<String> 的消费者。当消费者访问列表元素时,可能会稍后出现 ClassCastException

总结

如果你想防止参数化类型不匹配,只能在编译时进行,为了实现这一点,你必须将对象和消费者置于相同的语法上下文中,可能需要进行大规模的重构。

在运行时,只能防止原始类型不匹配。如果你想这样做,对于参数化类型,它会迫使你在语法上使用一些丑陋的语法(将类对象强制转换为参数化类类型),以满足编译器的要求。

就个人而言,我会放弃这个防护的想法。

  • 它在编译时没有帮助(它只检查是否能够将与 consumer 匹配的 clazz 传递给 function,而不检查实际对象是否匹配)。

  • 它可以在运行时检查一些错误情况。但是在像 List<String> 这样的情况下,它仍然无法防止你在稍后被消费者抛出 ClassCastException,这也是在没有防护的情况下发生的情况,只是发生的情况更多。

英文:

I don't think it's possible to achieve the degree of type safety you aim at.

Let's distinguish compile time and run time.

Compile Time

At compile time, the compiler checks whether the type of clazz matches the consumer's type parameter T. But that doesn't work well with types T that themselves have generics type parameters, e.g. List&lt;String&gt;, as there is no List&lt;String&gt;.class syntax in Java. So, here the best you can do is cast the List.class to Class&lt;List&lt;String&gt;&gt;.

The object obj that gets operated upon in your function isn't visible in the method's signature, so at compile time there's no chance to check whether its type matches the consumer.

There's nothing stopping you from calling function(String.class, stringConsumer) and having an Integer object. There's no chance the compiler will detect that mistake.

Run Time

At run time, there is no generics information. That concept is called "type erasure". So, something that at compile time appears as a List&lt;String&gt;, at runtime is just a List.

As at compile time, the obj is not visible at the function invocation, you moved the type check to run time, using the Class.cast() method.

If at compile time your obj is a List&lt;String&gt;, a List&lt;Integer&gt;, or a List&lt;Whatever&gt;, at run time it's just a raw List without parameters, and the class you are checking against also is just the raw List.class, without any type parameters. The cast() method will accept all these different lists.

So, at run time there's no way to reject a List&lt;Integer&gt; from entering a consumer expecting a List&lt;String&gt;. You'll probably get a ClassCastException later when the consumer accesses the list elements.

Summary

If you want to guard against mismatch of parameterized types, you can only do so at compile time, and to make that possible you have to bring object and consumer into the same syntactic context, probably meaning a big refactoring.

At run time, you can only guard against mismatch of raw types. If you want to do that, it forces you to use some ugly syntax in case of parameterized types (casting the class object to a parameterized class type), just to satisfy the compiler.

Personally, I'd give up the guarding idea.

  • It doesn't help at compile time (it only checks whether you were able to pass a clazz matching the consumer into function, not whether the actual object matches).

  • It can check for some error cases at run time. But in cases like List&lt;String&gt;, it still can't protect you from getting a ClassCastException later, thrown by the consumer. And that's what happens without your guard as well, just in more cases.

huangapple
  • 本文由 发表于 2020年9月30日 17:13:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/64134450.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定