通用函数用于遍历视图层次结构并查找特定类型的包含视图。

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

generic function to traverse a View hierarchy and find contained Views of a specific type

问题

My outer problem, in Java 1.8, is I want to traverse (or spider) a tree of View objects and call a callback on each one of a specific type. Here's an example caller:

findEachView(main.popupForm, (EditText v) -> {
    CharSequence error = v.getError();

    if (error != null)
        assertNull((String) v.getTag(), error.toString());
});

That is supposed to traverse all the controls on the popupForm and, for each one that is an EditText, call my callback which asserts that it does not have an error.

The inner problem is my generic method to do it has a syntax error:

static <T extends View> void findEachView(@NonNull ViewGroup view, @NonNull Consumer<T> viewAdjustorVisitor) {
    for (int i = 0; i < view.getChildCount(); i++) {
        View v = view.getChildAt(i);

        if (v instanceof T)  // ERROR:  reifiable types in instanceof are not supported in -source 8
            viewAdjustorVisitor.accept((T) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, viewAdjustorVisitor);
    }
}

So my outer question is: How to efficiently and typesafely rip every View of the given type?

And my inner question is: How to get the generics to not throw away type information and let me call instanceof on a type that I know is reified?


We can't use isAssignableFrom() because it takes an object of type T and my template doesn't have one. Its point is to downcast a View into a T, not mess with a T object that already exists. And I can't create a new object because T is "not reified":

if (v.getClass().isAssignableFrom(new T()))

So maybe if someone could answer the outer question instead of downvoting?


When I add the ? we get this, with the next syntax error. I think this is an unanswered question in Java 1.8 template theory:

static <T extends View> void findEachView(@NonNull
         ViewGroup viewGroup, @NonNull
         Consumer<Class<? extends View>> visitor) {
    for (int i = 0; i < viewGroup.getChildCount(); i++) {
        View v = viewGroup.getChildAt(i);

        if (null != (Class <? extends android.view.View>) v)  // ERROR:  incompatible types: View cannot be converted to Class<? extends View>
            visitor.accept((Class <? extends android.view.View>) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, visitor);
    }
}
英文:

My outer problem, in Java 1.8, is I want to traverse (or spider) a tree of View objects and call a callback on each one of a specific type. Here's an example caller:

        findEachView(main.popupForm, (EditText v) -> {
            CharSequence error = v.getError();

            if (error != null)
                assertNull((String) v.getTag(), error.toString());
        });

That is supposed to traverse all the controls on the popupForm and, for each one that is an EditText, call my callback which asserts that it does not have an error.

The inner problem is my generic method to do it has a syntax error:

static <T extends View> void findEachView(@NonNull ViewGroup view, @NonNull Consumer<T> viewAdjustorVisitor) {
    for (int i = 0; i < view.getChildCount(); i++) {
        View v = view.getChildAt(i);

        if (v instanceof T)  // ERROR:  reifiable types in instanceof are not supported in -source 8
            viewAdjustorVisitor.accept((T) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, viewAdjustorVisitor);
    }
}

So my outer question is: How to efficiently and typesafely rip every View of the given type?

And my inner question is: How to get the generics to not throw away type information and let me call instanceof on a type that I know is reified?


We can't use isAssignableFrom() because it takes an object of type T and my template doesn't have one. Its point is to downcast a View into a T, not mess with a T object that already exists. And I can't create a new object because T is "not reified":

if (v.getClass().isAssignableFrom(new T()))

So maybe if someone could answer the outer question instead of downvoting?


when I add the ? we get this, with the next syntax error. I think this is an unanswered question in Java 1.8 template theory

static <T extends View> void findEachView(@NonNull
         ViewGroup viewGroup, @NonNull
         Consumer<Class<? extends View>> visitor) {
    for (int i = 0; i < viewGroup.getChildCount(); i++) {
        View v = viewGroup.getChildAt(i);

        if (null != (Class <? extends android.view.View>) v)  // ERROR:  incompatible types: View cannot be converted to Class<? extends View>
            visitor.accept((Class <? extends android.view.View>) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, visitor);
    }
}

答案1

得分: 1

我认为你正在把这个问题弄得比必要复杂。最简单的解决方案就是传入你要查找的类。

一般来说,如果你发现需要类型信息但无法获得,那就将它传递进来,就像你为方法需要的任何其他信息一样。

由于类型擦除的原因,使用通用类型信息总是很棘手的,正如其他人所说的那样。在这些情况下,最好明确地传入类。如果你绝对需要知道某个东西的通用类型是什么,那么你可以像GSON一样使用子类包装器来通过 SubClass.class.getGenericSuperclass() 访问超类的通用信息。但这是一个不好的想法,会引入冗余并且不直观。

我还建议检查运行时类型信息是一种代码异味。你应该尽量避免这种情况,因为它会使优化变得更加困难(例如无法合并类),并且本质上是脆弱的。尽量看看是否可以通过接口默认方法、抽象基类或类似的方式完成相同的工作。如果有可能的话,使用标准的面向对象编程方法来完成工作,避免显式依赖于运行时类型信息。

// 目标是实现类似于 Android 开发中常见的方式。
findEachView(main.popupForm, EditText.class, (EditText v) -> { ... });

static <T extends View> void findEachView(
         @NonNull ViewGroup viewGroup,
         @NonNull Class<T> clz,
         @NonNull Consumer<? super T> visitor) {
  ...
  if (clz.isInstance(v)) {
    // 做一些事情
  }
  ...
}


谢谢!我将把我的工作代码添加到你的答案中,这样你就可以获得经验点数。

我的之前的代码使用字符串操作来匹配类名,所以我试图去掉那个参数:

findEachView(
        viewGroup,
        "EditText",
        (view) -> ((EditText) view).setError(null));

你的修复允许像这样调用,的确更清晰,因为它少了一个类型转换:

findEachView(
        viewGroup,
        EditText.class,
        (view) -> view.setError(null));

所以获胜的代码是:

static <T extends View> void findEachView(
                                 @NonNull ViewGroup view,
                                 Class<T> clz,
                                 @NonNull Consumer<? super T> visitor) {
    for (int i = 0; i < view.getChildCount(); i++) {
        View v = view.getChildAt(i);

        if (clz.isInstance(v))
            visitor.accept((T) v);  //  这个转换会产生一个委员会会忽略的警告

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, clz, visitor);
    }
}  //  注意这不会访问顶层项目

将聪明的 Lambda 调用转换为只返回一个简单的 View 列表是留给读者的练习。

英文:

I think you're making this more complicated than it needs to be. The simplest solution is just to pass in the class that you're looking for.

As a general rule, if you find that you need type information that isn't available, then pass it in. Much like you would do for any other information that a method needs.

Working with generic type information is always tricky because of type erasure, as others have stated. It's generally best to pass in the class explicitly in these situations. If you absolutely need to know what the generic type of something is, then you can do what GSON does and use a subclass wrapper to access the superclass's generic information via SubClass.class.getGenericSuperclass(). But this is a bad idea which introduces bloat and is non-intuitive.

I would also suggest that checking runtime type information is a code smell. You should do what you can to avoid this stuff since it makes it harder to optimize (cannot merge classes, for example) and is inherently fragile. Try to see if you can accomplish the same work with an interface default method, an abstract base class, or something similar. If at all possible, use standard OOP methods to do the work and avoid explicitly relying on runtime type information.

// Aim for something like this which is common in Android-land.
findEachView(main.popupForm, EditText.class, (EditText v) -> { ... });

static <T extends View> void findEachView(
         @NonNull ViewGroup viewGroup,
         @NonNull Class<T> clz,
         @NonNull Consumer<? super T> visitor) {
  ...
  if (clz.isInstance(v)) {
    // do stuff
  }
  ...
}


Thanks! I'm going to add my working code to your answer so you get the experience points.

My previous code used string surgery to match the class name, so I was trying to take that argument out:

    findEachView(
            viewGroup,
            "EditText",
            (view) -> ((EditText) view).setError(null));

Your fix allows a call like this, which is indeed cleaner because it has one fewer typecast:

    findEachView(
            viewGroup,
            EditText.class,
            (view) -> view.setError(null));

So the winning code is:

static <T extends View> void findEachView(
                                 @NonNull ViewGroup view,
                                 Class<T> clz,
                                 @NonNull Consumer<? super T> visitor) {
    for (int i = 0; i < view.getChildCount(); i++) {
        View v = view.getChildAt(i);

        if (clz.isInstance(v))
            visitor.accept((T) v);  //  this cast gives a warning that the committee will ignore

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, clz, visitor);
    }
}  //  note this does not visit the top level item

Converting the clever lambda call to just returning a dumb list of Views is left as an exercise for the reader.

答案2

得分: 0

Java在泛型方面使用类型擦除,因此类的字节码表示中T的类型为java.lang.Object,因此instanceof对任何对象都返回true,并且编译器会拒绝。

最简单的修复方法是添加一个Class<? extends T>参数,并直接使用类对象进行运行时反射。

英文:

Java works using type erasure for generics, so the bytecode representation of the class has java.lang.Object as the type for T, so the instanceof will be true for any object and the compiler rejects.

Simplest fix is to add a Class&lt;? extends T&gt; argument and use runtime reflection using the class object directly

huangapple
  • 本文由 发表于 2023年6月16日 07:48:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/76486138.html
匿名

发表评论

匿名网友

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

确定