简体   繁体   中英

Variables not initialized properly when initializing it in an overriden abstract function called from constructor or init block

I hit a problem with some Kotlin code and I found out it was related to calling a method that assigns some variables from an init block (or a secondary constructor for that matter, either reproduces the problem).

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

Then doing:

val tx = TextureShader()
tx.dump()

prints:

0, 1, 2
404, 404, 404

The print statements are called in order from getUniforms to the dump call at the end. It's assigned fine in the getUniforms method, but when calling them just a few milliseconds later, they're suddenly set to the default value of (in this case) 404. This value can be anything though, but I use 404 because that's a value I know I won't use for testing in this particular MCVE.

I'm using a system that relies heavily on abstract classes, but calling some of these methods ( getUniforms is extremely important) is a must. If I add an init block in either BoilerplateShader or TextureShader with a call to getUniforms , it works fine. Doing a workaround with an init function (not an init block) called after object creation:

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

works fine. But that would involve the created instance manually calls it:

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

which isn't an option. Writing the code that causes problems in Kotlin in Java works like expected (considerably shortened code, but still reproducable):

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();
    }
}

and printing the value of the variable after initialization of both the variable and the class prints 0, as expected.

Trying to reproduce the same thing with an object produces the same result as with numbers when the var isn't lateinit . So this:

var test: String = ""

prints:

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

The last line is exactly as printed: the value if test is set to an empty String by default, so it shows up as empty.

But if the var is declared as a lateinit var :

lateinit var test: String

it prints:

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

I can't declare primitives with lateinit . And since it's called outside a constructor, it either needs to be initialized or be declared as lateinit .

So, is it possible to initialize primitives from an overridden abstract method without creating a function to call it?


Edit:

A comment suggested a factory method, but that's not going to work because of the abstraction. Since the attempted goal is to call the methods from the base class ( Shader ), and since abstract classes can't be initialized, factory methods won't work without creating a manual implementation in each class, which is overkill. And if the constructor is private to get it to work (avoid initialization outside factory methods), extending won't work ( <init> is private in Shader ).

So the constructors are forced to be public (whether the Shader class has a primary or secondary constructor, the child classes have to have a primary to initialize it) meaning the shaders can be created while bypassing the factory method. And, abstraction causes problems again, the factory method (having to be abstract) would be manually implemented in each child class, once again resulting in initialization and manually calling the init() method.

The question is still whether or not it's possible to make sure the non-lateinit and primitives are initialized when calling an abstract method from the constructor. Creating factory methods would be a perfect solution had there not been abstraction involved.

Note: The absolutely best idea is to avoid declaring objects/primitives in abstract functions called from the abstract class' constructor method, but there are cases where it's useful. Avoid it if possible.


The only workaround I found for this is using by lazy , since there are primitives involved and I can convert assignment to work in the blocks.

lateinit would have made it slightly easier, so creating object wrappers could of course be an option, but using by lazy works in my case.

Anyways, what's happening here is that the value assigned to the int in the constructor is later overridden by the fixed value. Pseudocode:

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

With lateinit, the problem is removed:

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

When I wrote the question, I thought Java worked differently. This was because I didn't initialize the variables there either (effectively, making them lateinit). When the class then is fully initialized, int x; doesn't get assigned a value. If it was declared as int x = 1234; , the same problem in Java occurs as here.

Now, the problem goes back to lateinit and primitives; primitives cannot be lateinit. A fairly basic solution is using a data class:

data class IntWrapper(var value: Int)

Since the value of data classes can be unpacked:

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.

Now, since there's an instance with an object (not a primitive), lateinit can be used. However, this isn't particularly efficient since it involves another object being created.

The only remaining option: by lazy .

Wherever it's possible to create initialization as a function, this is the best option. The code in the question was a simplified version of OpenGL shaders (more specifically, the locations for uniforms). Meaning this particular code is fairly easy to convert to a by lazy block:

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

Depending on the case though, this might not be feasible. Especially since by lazy requires a val , which means it isn't possible to change it afterwards. This depends on the usage though, since it isn't a problem if it isn't going to change.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM