英文:
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
<T> void function(Class<T> clazz, Consumer<T> 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<String> c = ...;
function(String.class, c);
Unfortunately, the following code does not work:
Consumer<List<String>> c = ...;
function(List.class, c);
This is because List.class
has type Class<List>
, not class Class<List<String>>
.
I can change the type of c
to Consumer<List>
, 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<?> clazz, Consumer<T> consumer)
would not be acceptable because the user can easily provide the wrongclazz
. And invoking asfunction((Class<List<String>>)List.class, c)
is also not acceptable because I could accidentally cast it asfunction((Class<Integer>)List.class, c)
and
this wrong cast would not even be caught at runtime. If there was a function that allows to castClass<C<T>>
toClass<C<U>>
but not toClass<D>
, 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<String>
and not aList<Integer>
. 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 makingfunction
more complex) becausefunction
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
isfunction
.
答案1
得分: 2
这只使用 Class<T>
参数是无法实现你想要的功能的。你需要一个超类型标记来代替。
正如链接的文章中所解释的,以下是你可能实现这种功能的方式:
public abstract class TypeToken<T> {
private final Type type;
public TypeToken() {
Type superclass = getClass().getGenericSuperclass();
type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
在Java中,你无法获取关于实例的泛型类型变量的信息,但你可以获取关于超类型的泛型类型变量的一些信息。请注意,TypeToken
类是抽象的。以下是你在示例中应该如何使用它:
<T> void function(TypeToken<T> token, Consumer<T> consumer)
Consumer<List<String>> c = ...;
function(new TypeToken<List<String>>() { }, c);
这意味着你将一个 TypeToken
的子类传递给了你的函数。
最后,在 function
内部,你可以提取一个 Class
实例,并进行类型转换,如下所示:
Class<T> clazz;
if (type instanceof ParameterizedType)
clazz = (Class<T>) ((ParameterizedType) type).getRawType();
else if (typeU instanceof Class)
clazz = (Class<T>) 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<T>
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<T> {
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:
<T> void function(TypeToken<T> token, Consumer<T> consumer)
Consumer<List<String>> c = ...;
function(new TypeToken<List<String>>() { }, 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<T> clazz;
if (type instanceof ParameterizedType)
clazz = (Class<T>) ((ParameterizedType) type).getRawType();
else if (typeU instanceof Class)
clazz = (Class<T>) type;
else
throw new RuntimeException("Error handling omitted");
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
。
就像在编译时一样,obj
在 function
调用时不可见,所以你将类型检查移到了运行时,使用 Class.cast()
方法。
如果在编译时,你的 obj
是 List<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<String>
, as there is no List<String>.class
syntax in Java. So, here the best you can do is cast the List.class
to Class<List<String>>
.
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<String>
, 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<String>
, a List<Integer>
, or a List<Whatever>
, 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<Integer>
from entering a consumer expecting a List<String>
. 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 theconsumer
intofunction
, not whether the actual object matches). -
It can check for some error cases at run time. But in cases like
List<String>
, it still can't protect you from getting aClassCastException
later, thrown by the consumer. And that's what happens without your guard as well, just in more cases.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论