Why would I get a null reference exception?

In my project, I have the below code structure. Being a unity project, each class is of course in its own file with appropriate uses and method implementations. My question is, why am I getting a null reference exception (at the bottom). When debugging, the line in question shows both icons and database are null - even after the call to buildDatabase()

What am I missing? Is there a better way to approach this?

Thanks,

public abstract class CGenericDatabase : Monobehaviour {
  [SerializeField]protected TextAsset _textFile;
  publict TextAsset TextFile { get => _textFile; set { _textFile = value; ReloadTextFile(); }
  public void ReloadTextFile() { throw new NotImplementedException(); }
}

public abstract class GenericDatabase<T> : CGenericDatabase where T: class {
  [SerializeField]protected List<T> _list;
  protected override void ReloadTextFile() { ... }
  
  protected abstract T ParseString(String line);
  public abstract T Find(String item_name);  
}

public class IconDatabase : GenericDatabase<CardIcon> {
  protected override CardIcon ParseString(String line) { ... }
  public override CardIcon Find(String item_name) { ... }
}

public abstract class IconDatabaseUser<T> : GenericDatabase<T> where T: class {
  [SerializeField]protected IconDatabase _icons;
  public IconDatabase icons {
    get { return _icons; }
	set { _icons = value; ReloadTextFile(); }
  }
}

public class PowerDatabase : IconDatabaseUser<CardPower> {
  protected override CardPower ParseString(String line) { ... }
  public override CardPower Find(String item_name) { ... }
}

public class RarityDatabase : IconDatabaseUser<CardRarity> {
  protected override CardRarity ParseString(String line) { ... }
  public override CardRarity Find(String item_name) { ... }
}

public class CanonDatabase : IconDatabaseUser<CardCanon> {
  protected override CardCanon ParseString(String line) { ... }
  public override CardCanon Find(String item_name) { ... }
}

public class DatabaseStore : GenericSingleton<DatabaseStore> {
  [SerializeField] private TextAsset _iconsText;
  [SerializeField] private TextAsset _powersText;
  [SerializeField] private TextAsset _raritiesText;
  [SerializeField] private TextAsset _canonsText;
  [SerializeField] private TextAsset _cardsText;

  public IconDatabase _icons { get; private set; }
  public PowerDatabase _powers { get; private set; }
  public RarityDatabase _rarities { get; private set; }
  public CanonDatabase _canons { get; private set; }
  public CardDatabase _cards { get; private set; }

  private void Start() {
    buildDatabase(_icons).TextFile = _iconsText; 
    buildUserDatabase<PowerDatabase,CardPower>(_powers, _powersText);
    buildUserDatabase<RarityDatabase,CardRarity>(_rarities, _raritiesText);
    buildUserDatabase<CanonDatabase,CardCanon>(_canons, _canonsText);
    // buildUserDatabase<CardDatabase,CardDefinition>(_cards, _cardsText);
  }

  private T buildDatabase<T>(T database) where T: CGenericDatabase {
    if (database != null)
    DestroyImmediate(database);

    database = gameObject.AddComponent<T>(); 
    return database;
  }
    
  private void buildUserDatabase<T,U>(T database, TextAsset file) where U: class where T: IconDatabaseUser<U> {
    buildDatabase(database);
    database.iconDatabase = _icons; // NullReferenceException: Object reference not set to an instance of an object
    database.TextFile = file;
  }
}

It looks like _icons is not being instantiated in the DatabaseStore class, so when buildUserDatabase is called and it attempts to set the iconDatabase property on database, it is actually setting it on a null reference.

One possible solution would be to initialize _icons in the DatabaseStore constructor, like this:

public class DatabaseStore : GenericSingleton<DatabaseStore> {
  // Other code here...

  public IconDatabase _icons { get; private set; }

  public DatabaseStore() {
    _icons = new IconDatabase();
  }

  // Rest of the code here...
}

Alternatively, you could change the buildDatabase method to return the newly-created T instance, and then assign it to _icons in the Start method, like this:

private void Start() {
  _icons = buildDatabase<IconDatabase>(_icons);
  _icons.TextFile = _iconsText; 
  buildUserDatabase<PowerDatabase,CardPower>(_powers, _powersText);
  buildUserDatabase<RarityDatabase,CardRarity>(_rarities, _raritiesText);
  buildUserDatabase<CanonDatabase,CardCanon>(_canons, _canonsText);
  // buildUserDatabase<CardDatabase,CardDefinition>(_cards, _cardsText);
}

private T buildDatabase<T>(T database) where T: CGenericDatabase {
  if (database != null) {
    DestroyImmediate(database);
  }

  database = gameObject.AddComponent<T>(); 
  return database;
}

It looks like _icons is not being instantiated in the DatabaseStore class, so when buildUserDatabase is called and it attempts to set the iconDatabase property on database, it is actually setting it on a null reference.

One possible solution would be to initialize _icons in the DatabaseStore constructor, like this:

Copy code
public class DatabaseStore : GenericSingleton {
// Other code here…

public IconDatabase _icons { get; private set; }

public DatabaseStore() {
_icons = new IconDatabase();
}

// Rest of the code here…
}
Alternatively, you could change the buildDatabase method to return the newly-created T instance, and then assign it to _icons in the Start method, like this:

Copy code
private void Start() {
_icons = buildDatabase(_icons);
_icons.TextFile = _iconsText;
buildUserDatabase(_powers, _powersText);
buildUserDatabase(_rarities, _raritiesText);
buildUserDatabase(_canons, _canonsText);
// buildUserDatabase(_cards, _cardsText);
}

private T buildDatabase(T database) where T: CGenericDatabase {
if (database != null) {
DestroyImmediate(database);
}

database = gameObject.AddComponent();
return database;
}
In either case, the _icons property will be properly initialized, and the NullReferenceException should be avoided.

I hope this helps! Let me know if you have any other questions.

I would like to thank @Maseeda for his contribution to this solution. He pointed me in the right direction. As such, I wish that I could mark his response as the answer since he was so helpful; however, I want to illustrate the entire code structure change that was required so that others who may be having similar issues can see what I had to do to fix this problem.

So, with that in mind, here’s the new code structure with some of the missing classes that were implied earlier. In the earlier code, CardDatum did not exist and is added now so that all of the other classes have a common parent in the hierarchy.

public class CardDatum {}
public class CardIcon : CardDatum {...}
public class CardPower : CardDatum {...}
public class CardRarity : CardDatum {...}
public class CardCanon : CardDatum {...}
public class CardDefinition : CardDatum {...}

public class GenericSingleton<> : monobehaviour {...}

public abstract class CGenericDatabase : Monobehaviour {
  [SerializeField]protected TextAsset _textFile;
  publict TextAsset TextFile { get => _textFile; set { _textFile = value; ReloadTextFile(); }
  public void ReloadTextFile() { throw new NotImplementedException(); }
}

public abstract class GenericDatabase<T> : CGenericDatabase where T: CardDatum {
  [SerializeField]protected List<T> _list;
  protected override void ReloadTextFile() { ... }
  
  protected abstract T ParseString(String line);
  public abstract T Find(String item_name);  
}

public class IconDatabase : GenericDatabase<CardIcon> {
  protected override CardIcon ParseString(String line) { ... }
  public override CardIcon Find(String item_name) { ... }
}

public abstract class IconDatabaseUser<T> : GenericDatabase<T> where T: CardDatum {
  [SerializeField]protected IconDatabase _icons;
  public IconDatabase icons {
    get { return _icons; }
	set { _icons = value; ReloadTextFile(); }
  }
}

public class PowerDatabase : IconDatabase<CardPower> {
  protected override CardPower ParseString(String line) { ... }
  public override CardPower Find(String item_name) { ... }
}

public class RarityDatabase : IconDatabase<CardRarity> {
  protected override CardRarity ParseString(String line) { ... }
  public override CardRarity Find(String item_name) { ... }
}

public class CanonDatabase : IconDatabase<CardCanon> {
  protected override CardCanon ParseString(String line) { ... }
  public override CardCanon Find(String item_name) { ... }
}

public class DatabaseStore : GenericSingleton<DatabaseStore> {  
  [SerializeField] private TextAsset _iconsText;
  [SerializeField] private TextAsset _powersText;
  [SerializeField] private TextAsset _raritiesText;
  [SerializeField] private TextAsset _canonsText;
  [SerializeField] private TextAsset _cardsText;

  public IconDatabase _icons { get; private set; }
  public PowerDatabase _powers { get; private set; }
  public RarityDatabase _rarities { get; private set; }
  public CanonDatabase _canons { get; private set; }
  public CardDatabase _cards { get; private set; }

  private void Start() {
    // The line below does NOT work... I recall reading about it but don't remember why it don't!
    // buildDatabase<IconDatabase,CardIcon>(_icons).TextFile = _iconsText; 

    _icons = buildDatabase<IconDatabase,CardIcon>(_icons);
    _icons.TextFile = _iconsText;       

    _powers = buildUserDatabase<PowerDatabase,CardPower>(_powers, _powersText);
    _rarities = buildUserDatabase<RarityDatabase,CardRarity>(_rarities, _raritiesText);
    _canons = buildUserDatabase<CanonDatabase,CardCanon>(_canons, _canonsText);
    _cards = buildCardsDatabase(_cards, _cardsText);
  }

  private T buildDatabase<T,U>(T database) where U: CardDatum where T: GenericDatabase<U> {
    database = gameObject.AddComponent<T>(); 
    return database;
  }
  
  private T buildUserDatabase<T,U>(T database, TextAsset file) where U: CardDatum where T: IconDatabaseUser<U> {
    database = buildDatabase<T,U>(database);
    database.iconDatabase = _icons;
    database.TextFile = file;
    return database;
  }

  private CardDatabase buildCardsDatabase(CardDatabase database, TextAsset file) {
    database = buildDatabase<CardDatabase, CardDefinition>(database);
    database.iconDatabase = _icons;
    database.rarityDatabase = _rarities;
    database.canonDatabase = _canons;
    database.powerDatabase = _powers;
    database.TextFile = file;
    return database;
  }
}

Notice the new DatabaseStore code. This is where @Maseeda was helpful. I went back to returning type T from the buildDatabase() function(s) and assigning those results to the class values. It only seems to be required for the IconsDatabase (first build call) but to keep it consistent, I made them all do this.

Also, note how the generic methods<> have a specific type constraint requirement for the U type. This was the reason for the CardDatum class inclusion.

In short, a slight rethinking of the structure / approach was needed. Thanks again to @Maseeda for the help.

Regards,