在从构造函数或init块

时间:2017-12-31 21:54:05

标签: kotlin

我遇到了一些Kotlin代码的问题,我发现它与调用一个从init块分配一些变量的方法(或者是二次构造函数,或者重现问题)有关。

MCVE:

abstract class Shader(/*Input arguments omitted for the sake of an MCVE*/){

    init{
        //Shader loading and attaching, not relevant
        bindAttribs()//One of the abstract methods. In my actual program, this uses OpenGL to bind attributes
        //GLSL program validation
        getUniforms()//Same as the previous one: abstract method using GL calls to get uniforms. This gets locations so an integer is set (the problem)

    }
    abstract fun getUniforms();//This is the one causing problems
    abstract fun bindAttribs();//This would to if primitives or non-lateinit vars are set
}

abstract class BoilerplateShader() : Shader(){
    var loc_projectionMatrix: Int = 404//404 is an initial value. This can be anything though
    var loc_transformationMatrix: Int = 404
    var loc_viewMatrix: Int = 404

    override fun getUniforms(){
        //These would be grabbed by using glGetUniformLocations, but it's reproducable with static values as well
        loc_projectionMatrix = 0
        loc_transformationMatrix = 1
        loc_viewMatrix = 2
        println(loc_projectionMatrix.toString() + ", " + loc_transformationMatrix + ", " + loc_viewMatrix)
    }

    //debug method, only used to show the values
    fun dump(){
        println(loc_projectionMatrix.toString() + ", " + loc_transformationMatrix + ", " + loc_viewMatrix)
    }

}

class TextureShader() : BoilerplateShader(){

    override fun bindAttribs() {
        //This doesn't cause a problem even though it's called from the init block, as nothing is assigned
        //bindAttrib(0, "a_position");
        //bindAttrib(1, "a_texCoord0");
    }
}

//Other repetitive shaders, omitted for brevity

然后做:

val tx = TextureShader()
tx.dump()

打印:

0, 1, 2
404, 404, 404

从getUniforms到最后的转储调用按顺序调用print语句。它在getUniforms方法中分配得很好,但是在几毫秒之后调用它们时,它们会突然设置为默认值(在这种情况下)404.此值可以是任何值虽然,但我使用的是404,因为我知道这个值不会用于在这个特定的MCVE中进行测试。

我使用的系统严重依赖抽象类,但调用其中一些方法(getUniforms非常重要)是必须的。如果我通过调用BoilerplateShaderTextureShadergetUniforms中添加初始化块,则可以正常工作。使用在创建对象后调用的init函数(不是init块)进行解决方法:

fun init(){
    bindAttribs();
    getUniforms();
}

工作正常。但这将涉及创建的实例手动调用它:

val ts = TexturedShader();
ts.init();
ts.dump()

这不是一个选项。编写导致Java中Kotlin出现问题的代码就像预期的那样(代码大大缩短,但仍然可以重现):

abstract class Shader{
    public Shader(){
        getUniforms();
    }

     public abstract void getUniforms();
}

abstract class BoilerplateShader extends Shader{
    int loc_projectionMatrix;//When this is initialized, it produces the same issue as Kotlin. But Java doesn't require the vars to be initialized when they're declared globally, so it doesn't cause a problem
    public void getUniforms(){
        loc_projectionMatrix = 1;
        System.out.println(loc_projectionMatrix);
    }
    //and a dump method or any kind of basic print statement to print it after object creation
}

class TextureShader extends BoilerplateShader {
    public TextureShader(){
        super();
    }
}

并在变量和类的初始化之后打印变量的值,如预期那样打印0。

尝试使用对象重现相同的事物会产生与数字时相同的结果,当var不是 时。所以这个:

var test: String = ""

打印:

0, 1, 2, test
404, 404, 404, 

最后一行与打印完全一样:默认情况下test设置为空字符串时的值,因此显示为空。

但是如果var被声明为lateinit var

lateinit var test: String

打印:

0, 1, 2, test
404, 404, 404, test

can't declare primitives with lateinit。由于它在构造函数外部调用,因此需要初始化或声明为lateinit

那么,是否可以从重写的抽象方法初始化基元而无需创建调用它的函数?

编辑:

评论提出了一种工厂方法,但由于抽象而无法工作。由于尝试的目标是从基类(Shader)调用方法,并且由于抽象类无法初始化,因此工厂方法无法在每个类中创建手动实现,这太过分了。如果构造函数是私有的以使其工作(避免在工厂方法之外进行初始化),那么扩展工作(<init> is private in Shader)。

因此构造函数必须是公共的(无论Shader类是否具有主构造函数或辅助构造函数,子类必须具有初始化它的初始化),这意味着可以在绕过工厂方法时创建着色器。并且,抽象再次导致问题,工厂方法(必须是抽象的)将在每个子类中手动实现,再次导致初始化并手动调用init()方法。

问题仍然是,当从构造函数调用抽象方法时,是否可以确保初始化非lateinit和基元。如果没有涉及抽象,创建工厂方法将是一个完美的解决方案。

1 个答案:

答案 0 :(得分:1)

注意:绝对最好的想法是避免在抽象类的构造函数方法中调用抽象函数中声明对象/基元,但有些情况下它很有用。如果可能,请避免使用它。

我找到的唯一解决方法是使用by lazy,因为涉及原语,我可以将赋值转换为块中的工作。

lateinit会让它稍微容易一些,所以创建对象包装器当然可以作为一种选择,但在我的情况下使用by lazy是有效的。

无论如何,这里发生的事情是,构造函数中赋值给int的值稍后会被固定值覆盖。伪代码:

var x /* = 0 */
constructor() : super.constructor()//x is not initialized yet
super.constructor(){
    overridden function();
}
abstract function()
overridden function() {
    x = 4;
}
// The assignment if `= 0` takes place after the construction of the parent, setting x to 0 and overriding the value in the constructor

通过lateinit,问题被删除了:

lateinit var x: Integer//x exists, but doesn't get a value. It's assigned later
constructor() : super.constructor()
super.constructor(){
    overridden function()
}
abstract function()
overridden function(){
    x = Integer(4);//using an object here since Kotlin doesn't support lateinit with primtives
}
//x, being lateinit and now initialized, doesn't get re-initialized by the declaration. x = 4 instead of 0, as in the first example

当我写这个问题时,我认为Java的工作方式不同。这是因为我没有在那里初始化变量(有效地,使它们成为lateinit)。当该类完全初始化时,int x;不会被赋值。如果它被声明为int x = 1234;,则Java中出现的问题与此处相同。

现在,问题可以追溯到晚期和原始;原始人不能迟到。一个相当基本的解决方案是使用数据类:

data class IntWrapper(var value: Int)

由于可以解压缩数据类的值:

var (value) = intWrapperInstance//doing "var value = ..." sets value to the intWrapperInstance. With the parenthesis it works the same way as unpacking the values of a pair or triple, just with a single value.

现在,由于存在一个带有对象(不是原始对象)的实例,因此可以使用lateinit。但是,这不是特别有效,因为它涉及创建另一个对象。

唯一剩下的选项:by lazy

无论何时可以将初始化作为函数创建,这都是最佳选择。问题中的代码是OpenGL着色器的简化版本(更具体地说,是制服的位置)。这意味着这个特定的代码很容易转换为by lazy块:

val projectionMatrixLocation by lazy{
    glGetUniformLocation(program, "projectionMatrix")
}

根据具体情况,这可能不太可行。特别是因为by lazy需要val,这意味着之后无法更改它。这取决于使用情况,因为如果它不会改变则不是问题。