Search code examples
c#arraysimmutabilityimmutable-collections

ImmutableArray how to use properly


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?


Solution

  • 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
    );