Array.has() for null objects behaves differently to checking null object

Godot Version

v4.4.beta4.official [93d270693]

Question

I sometimes check if all required parameters of a function are set and skip it if any is missing.

One way to do that is:

func someFunction() -> void:
	if [requiredProp1, requiredProp2].has(null):
		return

	# code that gets executed when the above passes

But that doesn’t always work when the checked property is a null object (prints as <Object#null>), for example, if you set a new Texture2D and then want to immediately call get_image(), e.g.

var texture_image := texture.get_image()

the return value may initially still be a null object. I suspect that setting a new Texture2D may take some time to be ready, and so:

[texture_image, etc].has(null)

doesn’t catch that null object.

There are workarounds, for example:

[texture_image, etc].any(func (item:Variant) -> bool: return item == null)

or individually, e.g.

texture_image == null

work just fine.

Do I misunderstand the use case for Array.has() or is this something that should behave differently?

I have the workaround, but it’s just something that annoys me because I don’t fully understand it.

Here’s a simplified real-world example that updates a CollisionShape3D based on a height map:

@tool
extends CollisionShape3D


@export var terrain_map:Texture2D:
	set(value):
		terrain_map = value
		update()

@export var terrain_amplitude:float = 25.0:
	set(value):
		terrain_amplitude = value
		update()

@export var surface_map:Texture2D:
	set(value):
		surface_map = value
		update()

@export var surface_amplitude:float = 2.0:
	set(value):
		surface_amplitude = value
		update()

func update() -> void:
	if [terrain_map, surface_map].has(null):
		# skip this function until all required properties are set; this works fine
		prints("Updating ground_collision_shape failed. Not all required properties are set.")
		return

	var terrain_map_image := terrain_map.get_image()
	var surface_map_image := surface_map.get_image()

	if [terrain_map_image, surface_map_image].has(null):
		# this does not work reliably because terrain_map_image may be a null object <Object#null>, even if terrain_map is clearly set
		prints("Updating ground_collision_shape failed. Cannot get image from terrain_map or surface_map.")
		return

	# if the above check for terrain_map_image fails to detect the null object, the following line will create a fatal error,
	# "Cannot call method 'get_width' on a null value."
	shape.map_width = terrain_map_image.get_width()
	shape.map_depth = terrain_map_image.get_height()

	# updating the CollisionShape3D shape based on the terrain map and a terrain amplitude
	terrain_map_image.convert( Image.FORMAT_RF )
	surface_map_image.convert( Image.FORMAT_RF )
	shape.update_map_data_from_image(terrain_map_image, 0, terrain_amplitude)

From the docs:

“For performance reasons, the search is affected by the value’s Variant.Type. For example, 7 (int) and 7.0 (float) are not considered equal for this method.”

My guess it’s the same with null. A Texture2D null is probably not the same as a general Variant null. I’m not sure that’s the reason though.

1 Like

Could you have an or in there to check for <Object#null>? Such as

if [requiredprop1, requiredprop2].has(null) or\
[requiredprop1, requiredprop2].has(<Object#null>):
    return
#or something similar

I don’t think the null check will do what you want, ultimately. What you’ve said is:

  • you have some parameters that might be null
  • for some parameters, that’s bad, for others it could be fine

There isn’t really enough information in .has() to tell you what you want; all it will tell you is if there’s at least one null in the list you’ve built.

It seems like in the above example what you want is:

if !surface_map_image: return

if terrain_map_image:
     terrain_map_image.convert(...stuff...)

surface_map_image.convert(...stuff...)

[...]

Building a list and then checking only whether the list contains at least one null is throwing away information you need to make the decisions you want to make.

If you have something more complex, you can:

if !foo || !bar || !baz: return  # all must be non-null

If it exits when at least one of the parameters is null, then all parameters must be non-null. Both work.

Ah, I think I misunderstood the question.

If the problem is that the return value from terrain_map.get_image() is kind of null, but is <Object#null>, and that’s (apparently?) not quite the same…

The fix for this is probably just directly checking:

if !terrain_image_map: return`

That ought to work, where the .has(null) comparison might not; if (as @ratrogue points out above) null and <Object#null> don’t have identical representations, and assuming .has() is just marching the list doing an equality check, the .has() check will be unreliable.

Thank you all for your replies, and sorry for my very late response. I’ve been moving house, so that was distracting …

I think the assumption is correct that checking for .has(null) doesn’t necessarily match all “null-ish” values such as the return value of terrain_map.get_image() which prints as <Object#null>.

So I think to keep working with my array-based code approach, instead of .has I will use the more reliable .any with a custom lambda function, i.e.

[texture_image, etc].any(func (item:Variant) -> bool: return item == null)

The only thing to add, I’m often hesitant to check with !, for example, if !terrain_image_map: return.

In this particular case, it’s not a problem, but in a more general-purpose approach, if the checked value was 0 or an empty string "", the check would return, even though 0 or "" may be perfectly valid. That has bitten me before and caused false positives. For that reason, I usually prefer to check for an explicit null value.

it seems very unconventional to me to specifically to create an array just for null checking.

Wouldn’t it be better to just check the values separately, so you directly see which values fail?

And if you really don’t need to know it exactly, why not just use:

if terrain_map_image == null || surface_map_image == null:

?