ImmutableArray how to use properly

1.4k Views Asked by At

I'm trying to create an array of constant values that CANNOT NEVER change. I try something like this....

public class ColItem
{
    public string Header { get; set; }              // real header name in ENG/CHI
    public string HeaderKey { get; set; }           // header identifier
    public bool IsFrozen { get; set; } = false;
    public bool IsVisible { get; set; } = true;
    public int DisplayIdx { get; set; }
}

public struct DatagridSettingDefault
{
    public static ImmutableArray<ColItem> DataGrid_MarketPriceTab = ImmutableArray.Create(new ColItem[] {
        new ColItem { HeaderKey = "ORDER_ID", IsFrozen = true, DisplayIdx = 0 },
        new ColItem { HeaderKey = "PRICE_RENAMEPROMPT", IsFrozen = false, DisplayIdx = 1 },
        new ColItem { HeaderKey = "PRICETBL_TRADESTATENO", IsFrozen = false, DisplayIdx = 2 },
        new ColItem { HeaderKey = "PRICETBL_BIDQTY", DisplayIdx = 3 },
        new ColItem { HeaderKey = "PRICETBL_BID", DisplayIdx = 4 },
        new ColItem { HeaderKey = "PRICETBL_ASK", DisplayIdx = 5 },
        new ColItem { HeaderKey = "PRICETBL_ASKQTY", DisplayIdx = 6 }
}

However, this array STILL gets changed from somewhere (another thread) of the code. How can I make an array constant and absolutely cannot change through the lifetime of the program? I just need to initialize this during compile time and no matter what reference or other thread change it, it should not change. But my my case, it keeps changing.

Such as, it gets changed if I do something like this later in the code, or through another thread...

HeaderKey = col.Header.ToString(),
Header = "whatever"
IsFrozen = col.IsFrozen,
IsVisible = true,
DisplayIdx = col.DisplayIndex

How do I fix it?

4

There are 4 best solutions below

1
Dai On BEST ANSWER

You don't need ImmutableArray<T>. Just do this:

  • Change DataGrid_MarketPriceTab from a mutable field to a getter-only property.
  • I recommend using IReadOnlyList<T> instead of ImmutableArray<T> because it's built-in to the .NET's BCL whereas ImmutableArray<T> adds a dependency on System.Collections.Immutable.
  • Notice that the property is initialized inline as an Array (ColItem[]) but because the only reference to it is via IReadOnlyList<T> then the collection cannot be changed without using reflection.
    • Making it a { get; }-only property means that consumers of your class cannot reassign the property value.
    • If you do need to use a field (instead of a property) then make it a static readonly field.

You also need to make ColItem immutable - that can be done by making all the member-properties { get; } instead of { get; set; } (and ensuring their types are also immutable):

Like so:

// This is an immutable type: properties can only be set in the constructor.
public class ColItem
{
    public ColItem(
        string headerName, string headerKey, int displayIdx,
        bool isFrozen = false, bool isVisible = true
    )
    {
        this.Header     = headerName ?? throw new ArgumentNullException(nameof(headerName));
        this.HeaderKey  = headerKey ?? throw new ArgumentNullException(nameof(headerKey));
        this.IsFrozen   = isFrozen;
        this.IsVisible  = isVisible;
        this.DisplayIdx = displayIdx;
    }

    public string Header     { get; }
    public string HeaderKey  { get; }
    public bool   IsFrozen   { get; }
    public bool   IsVisible  { get; }
    public int    DisplayIdx { get; }
}

public struct DatagridSettingDefault // Why is this a struct and not a static class?
{
    public static IReadOnlyList<ColItem> DataGrid_MarketPriceTab { get; } = new[]
    {
        new ColItem( headerName: "Order Id", headerKey: "ORDER_ID", isFrozen: true, displayIdx: 0 ),
        new ColItem( headerName: "Price", headerKey: "PRICE_RENAMEPROMPT", IsFrozen:false, displayIdx:1 ),
        new ColItem( headerName: "Foo", headerKey: "PRICETBL_TRADESTATENO", IsFrozen:false, displayIdx:2 ),
        new ColItem( headerName: "Bar", headerKey: "PRICETBL_BIDQTY", displayIdx:3 ),
        new ColItem( headerName: "Baz", headerKey: "PRICETBL_BID", displayIdx:4 ),
        new ColItem( headerName: "Qux", headerKey: "PRICETBL_ASK", displayIdx:5 ),
        new ColItem( headerName: "Hmmm", headerKey: "PRICETBL_ASKQTY", displayIdx:6 )
    };
}

If all of that seems tedious to you, I agree! The good news is that if you're using C# 9.0 or later (which requires .NET 5, unfortunately) you can use Immutable Record Types, so the class ColItem class becomes record ColItem:

public record ColItem(
    string Header,
    string HeaderKey,
    bool   IsFrozen = false,
    bool   IsVisible = true,
    int    DisplayIdx
);
0
Visual Studio On

Make your ColItem class immutable. There are a few ways to this, but the most direct way is by removing the set keyword from your properties making them readonly

1
TheGeneral On

If you want to make an immutable reference type, you can either make all the properties private setters. Or in C# 9 you could use init only properties, which can be constructed with object initializers

public string Header { get; init; } 

Or you can take it one step further and create a Record which has several other advantages, such as With-expressions, Value-based equality and differ from struct with Inheritance

public record ColItem
{
   public string Header { get; init; } // real header name in ENG/CHI
   public string HeaderKey { get; init; } // header identifier
   public bool IsFrozen { get; init; } = false;
   public bool IsVisible { get; init; } = true;
   public int DisplayIdx { get; init; }
}

Usage

 var test = new DatagridSettingDefault();
 test.DataGrid_MarketPriceTab[0].HeaderKey = "asd";

Will generate the below compiler error

CS8852 Init-only property or indexer 'ColItem.HeaderKey' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

Then all that is left is choose an immutable collection / collection interface

0
Theodor Zoulias On

Here is how you can make the ColItem class immutable:

public class ColItem
{
    public string HeaderKey { get; }
    public bool IsFrozen { get; }

    public ColItem(string headerKey, bool isFrozen = false)
    {
        HeaderKey = headerKey;
        IsFrozen = isFrozen;
    }
}

By declaring only the get accessor, a property becomes immutable everywhere except in the type's constructor. (docs)

In C# 9 there is also the option for { get; init; } properties, that are mutable during initialization, and become immutable after that, as well as the new record types that are immutable by default.

And here is how you can make the static property DatagridSettingDefault.DataGrid_MarketPriceTab immutable, by marking it as readonly:

public class DatagridSettingDefault
{
    public static readonly ImmutableArray<ColItem> DataGrid_MarketPriceTab
        = ImmutableArray.Create(new ColItem[]
    {
        new ColItem ("ORDER_ID", true),
        new ColItem ("PRICE_RENAMEPROMPT", false),
        new ColItem ("PRICETBL_TRADESTATENO", false),
        new ColItem ("PRICETBL_BIDQTY"),
        new ColItem ("PRICETBL_BID"),
        new ColItem ("PRICETBL_ASK"),
        new ColItem ("PRICETBL_ASKQTY"),
    });
}