Skip to content

Conversation

dimitri-dev
Copy link
Contributor

@dimitri-dev dimitri-dev commented Apr 8, 2024

Uhhh, will probably remove the comments. But there are some valid concerns in the code.
My main question is: do you realize that returning a default of a struct is not a null value, but rather an empty instance?

TLDR: NullReferenceException in #97 is caused by an uninitialized HashSet.

@dimitri-dev dimitri-dev changed the title NullReferenceException NullReferenceException on MAUI Apr 8, 2024
@dimitri-dev
Copy link
Contributor Author

Mind taking a look at this fix @wenxi-zeng ?

@nmaricmoberg
Copy link

nmaricmoberg commented Apr 10, 2024

Have you got a moment to check this out? @MichaelGHSeg
I tested this in my app and the issues seem to have stopped appearing, but it would be nice to know this is an approved fix and get a release out for everyone.

@dimitri-dev dimitri-dev changed the title NullReferenceException on MAUI NullReferenceException when fetching System state Apr 17, 2024
@MichaelGHSeg
Copy link
Contributor

Sorry for the delay in responding. I've looked over the code and I'm hesitant to declare it 'approved'. I think that it will behave as expected, but there are a few other changes that need to be made for me to be confident that it won't fail quietly. I also want Wenxi's input and he should be back soon.

@dimitri-dev
Copy link
Contributor Author

@MichaelGHSeg I have only taken a look at the bits surrounding the issue/exception that occured.

If I can be of any more help, either by testing or by coding, let me know!

Copy link
Contributor

@wenxi-zeng wenxi-zeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the PR. it looks great and promising. we will test it on our end and ship it in the next release if all looks good.

Settings? returnSettings = null;
IState system = await Store.CurrentState<System>();

// I don't understand this as Store.CurrentState will at worst return a default(TState) which will be a default(System) in this case, hence this cast will always go through?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good question. this check here only serves for a conversion purpose that converts an IState to System. the check alone is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you can also just accept var system = await Store.CurrentState<System>() or even System system = await Store.CurrentState<System>() and it would be fine?

I mean, CurrentState accepts TState generic and also returns a Task, so whatever you call it with, it should return as?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. we were doing it with var but had to change it to the current way for compatibility reason. I believe accept it as System should just be fine. no idea why we didn't do it 🤦

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, coding style aside - the real question is why the conversion check exists, and why does this method return Settings? when the CurrentState method returns default(TState) or the TState instance, in this case System.

And the System struct holds a Settings struct, which cannot be null again.
Is the property in System supposed to be Settings? (a nullable struct) maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh. I see what you're saying. the design of the system properties are on purpose. it's the way we chose to write the code made that SettingsAsync to return Settings?. the function will be updated accordingly with the removal of the type check.

Settings? settings = await plugin.Analytics.SettingsAsync();

// This is basically never null, look at the comment in SettingsAsync
if (settings == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good performance optimization!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's not really a performance optimization if you are never returning a null object. :/

if (settings.HasValue && system._initializedPlugins.Count > 0)

// Check for nullability because CurrentState returns default(IState) which could make the .Count throw a NullReferenceException
if (system._initializedPlugins != null && system._initializedPlugins.Count > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised _initializedPlugins could be null, since we have a null check in the constructor that converts it to be an empty set if it's null. but from my previous investigation this line is indeed the line causing the NRE. it might be the the environment.

Copy link
Contributor Author

@dimitri-dev dimitri-dev Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. You have a constructor with 4 parameters, and an optionable HashSet parameter. When returning a default(struct), it targets the default constructor. To my understanding, since the parameters are not available when creating a default instance, it cannot call it - hence you get an "empty" object.

In this case, a solution also could be to just add a default constructor like so:

internal System() { _initializedPlugins = new(); }

And it would avoid this check.

@MichaelGHSeg MichaelGHSeg merged commit cd7404b into segmentio:main Apr 22, 2024
@MichaelGHSeg
Copy link
Contributor

Thanks!

@dimitri-dev dimitri-dev deleted the fix/hashset-null-reference branch May 6, 2024 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants