Как ссылаться на подкласс из статического метода суперкласса в Groovy

#groovy

#groovy

Вопрос:

Упрощенная версия того, что я пытаюсь сделать в Groovy:

 class Animal {
    static def echo() {
        println this.name  // ie "class.name"
    }
}

class Dog extends Animal {
}

class Cat extends Animal {
}

Dog.echo()
Cat.echo()

// Output:
//  => Animal
//  => Animal
//
// What I want:
//  => Dog
//  => Cat
  

Я думаю, что здесь я спрашиваю следующее: когда я вызываю статический метод для объекта, и
статический метод определен в суперклассе объекта, есть ли способ получить
фактический тип объекта?

Ответ №1:

Статический метод определяется не в контексте объекта, а в контексте класса. Вас может смутить наличие this в статическом методе Groovy. Однако это всего лишь синтаксический сахар, который в конечном итоге заменяет this.name на Animal.class.name .

Если вы скомпилируете Animal класс из вашего примера с включенной статической компиляцией, вы увидите, что он компилируется в следующий Java-эквивалент (результат после декомпиляции файла .class):

 //
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;

public class Animal implements GroovyObject {
    public Animal() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public static Object echo() {
        DefaultGroovyMethods.println(Animal.class, Animal.class.getName());
        return null;
    }
}
  

Вы можете видеть, что следующая строка в echo методе:

 DefaultGroovyMethods.println(Animal.class, Animal.class.getName());
  

работает непосредственно с Animal именем класса. Итак, с echo точки зрения метода, не имеет значения, сколько классов его расширяют. Пока эти классы вызывают echo метод, определенный в Animal классе, вы всегда будете видеть Animal напечатанный результат.

И это даже больше, чем это. Если вы используете следующий скрипт настройки компилятора:

config.groovy

 withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}
  

а затем скомпилируйте скрипт (назовем его script.groovy), используя этот параметр конфигурации, с помощью следующей команды:

 groovyc --configscript=config.groovy script.groovy
  

затем вы увидите что-то вроде этого после декомпиляции файла .class:

 //
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.Binding;
import org.codehaus.groovy.runtime.InvokerHelper;

public class script extends groovy.lang.Script {
    public script() {
    }

    public script(Binding context) {
        super(context);
    }

    public static void main(String... args) {
        InvokerHelper.runScript(script.class, args);
    }

    public Object run() {
        Animal.echo();
        return Animal.echo();
    }
}
  

Вы можете видеть, что, несмотря на то, что вы вызвали Dog.echo() и Cat.echo() в вашем Groovy-скрипте, компилятор заменил эти вызовы двойным Animal.echo() вызовом. Это произошло потому, что вызов этого статического метода в любом другом подклассе не имеет никакого значения.

Возможное решение: применение двойной отправки

Есть один способ получить ожидаемый результат — переопределить echo статический метод в Dog и Cat классе. Я могу предположить, что ваш реальный метод может выполнять нечто большее, чем примерный echo метод, который вы показали выше, поэтому вам может потребоваться вызвать супер echo метод из родительского класса. Но … есть две проблемы: (1) вы не можете использовать super.echo() в статическом контексте и (2) это не решает проблему, потому что родительский метод все еще работает в Animal контексте класса.’

Чтобы решить проблему такого рода, вы можете захотеть имитировать технику, называемую двойной отправкой. Короче говоря, когда у нас нет информации о вызывающем объекте в методе, который был вызван, давайте позволим вызывающему объекту передать эту информацию при вызове метода. Рассмотрим следующий пример:

 import groovy.transform.CompileStatic

@CompileStatic
class Animal {
    // This is a replacement for the previous echo() method - this one knows the animal type from a parameter
    protected static void echo(Class<? extends Animal> clazz) {
        println clazz.name
    }

    static void echo() {
        echo(Animal)
    }
}

@CompileStatic
class Dog extends Animal {
    static void echo() {
        echo(Dog)
    }
}

@CompileStatic
class Cat extends Animal {
    static void echo() {
        echo(Cat)
    }
}

Animal.echo()
Dog.echo()
Cat.echo()
  

Это может звучать как шаблонное решение — оно требует реализации echo метода в каждом подклассе. Однако он инкапсулирует echo логику в метод, который требует Class<? extends Animal> параметра, поэтому мы можем позволить каждому подклассу вводить свой конкретный подтип. Конечно, это не идеальное решение. Это требует реализации echo метода в каждом подклассе, но другого альтернативного способа нет. Другая проблема заключается в том, что это не мешает вам вызывать, Dog.echo(Animal) что вызовет тот же эффект, что и вызов Animal.echo() . Этот подход, подобный двойной отправке, больше похож на введение сокращенной версии echo метода, которая для простоты использует реализацию обычного статического echo метода.

Я не знаю, решает ли такой подход вашу проблему, но, возможно, это поможет вам найти окончательное решение.

Комментарии:

1. Очень информативная разбивка, и да, предложенный шаблон (поместить основную часть логики в суперкласс и реализовать небольшие оболочки в каждом подклассе) будет работать нормально. Я просто хотел убедиться, что я не пропустил более элегантный подход «groovy-er» — ваш ответ развеивает эту идею.