karenpayneoregon / containers-csharp

For teaching collections and similar topics

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Working with collections

For teaching about commonly used collection types.


The absolute best way to learn from what is presented is to read this entire readme file to get an idea of why programmers may selection specific types of containers and why they may not be good for certain task.

Many times the best solutions turn out to require more effort by a developer which pays back from optimization of memory along with better filling business requirements.


New developers to C# with little to no formal training when needing a collection/container will scour the web for a suitable collection and inevitably find List<T> or Array.

Dependent on the task, what comes next is

  • The container fits the task
  • The container fits the task but is slow performance-wise
  • After review, the container does not fulfill the task completely
    • Container allows modications to data that should not be modified, meaning an immutable vs mutable
    • Allows duplicates where no duplicates are permitted.
    • Does not work well with other sections of the application e.g. a report requires a List<T> and the choice was a DataTable

Looking at using a DataTable when a List<T> is needed the average developer hops back on the web and finds a language extension (Figure 1) that converts a DataTable to a List. This solves one problem but may lead to other issues such as time to perform the conversion when response time is critical.

Note: Although DataTable is old they still have a place in today's world. See the following for their events and change notification.

In the Visual Studio solution there is an exploration of common collections/containers with advantages and gochas.

This will be followed up with design patterns.

Figure 1

public static List<TSource> DataTableToList<TSource>(this DataTable table) where TSource : new()
{
    List<TSource> list = new();

    var typeProperties = typeof(TSource).GetProperties().Select(propertyInfo => new
    {
        PropertyInfo = propertyInfo,
        Type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType
    }).ToList();

    foreach (var row in table.Rows.Cast<DataRow>())
    {

        TSource current = new();

        foreach (var typeProperty in typeProperties)
        {
            object value = row[typeProperty.PropertyInfo.Name];
            object safeValue = value is null || DBNull.Value.Equals(value) ? null : Convert.ChangeType(value, typeProperty.Type);
            typeProperty.PropertyInfo.SetValue(current, safeValue, null);
        }

        list.Add(current);
    }

    return list;
}

This can be avoided by knowing both business requirements along with communication between components in various task.

Immutable collections .NET Core (C# 9)

In software development, an immutable object is one that once created, can't change.

Provides methods for creating an class instance that is immutable; meaning it cannot be changed once it is created.

In this solution see the following project where code is based off Mobile friendly weekly claims application.cfc component..

Mutable design note getters/setters

class OrderLine
{
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public float Discount { get; set; }

    public decimal Total
    {
        get
        {
            return Quantity * UnitPrice * (decimal) (1.0f - Discount);
        }
    }
}
class Order 
{     
    public Order()
    {
         Lines = new List<OrderLine>();
    } 

    public List<OrderLine> Lines { get; } 
}

Immutable design

class OrderLine
{
    public OrderLine(int quantity, decimal unitPrice, float discount)
    {
        Quantity = quantity;
        UnitPrice = unitPrice;
        Discount = discount;
    }

    public int Quantity { get; }

    public decimal UnitPrice { get; }

    public float Discount { get; }

    public decimal Total
    {
        get
        {
            return Quantity * UnitPrice * (decimal) (1.0f - Discount);
        }
    }
}

This new design requires that you create a new instance of an OrderLine whenever any of the values changes. You can make the design a bit more convenient by adding WithXxx methods that let you update individual properties without having to explicitly call the constructor yourself:

class OrderLine
{
    // ...

    public OrderLine WithQuantity(int value)
    {
        return value == Quantity
                ? this
                : new OrderLine(value, UnitPrice, Discount);
    }

    public OrderLine WithUnitPrice(decimal value)
    {
        return value == UnitPrice
                ? this
                : new OrderLine(Quantity, value, Discount);
    }

    public OrderLine WithDiscount(float value)
    {
        return value == Discount
                ? this
                : new OrderLine(Quantity, UnitPrice, value);
    }
}

Usage

OrderLine apple = new OrderLine(quantity: 1, unitPrice: 2.5m, discount: 0.0f);
OrderLine discountedAppled = apple.WithDiscount(.3f);

From Code magazine

The author has created a custom exception class which is ImmutableHashSet. Note HashSet are unordered as in this case we careless about ordering. Implementation provides custom exception message when attempting to change properties in a class instance.

public class InvalidDataTypeException : Exception
{
        public static ImmutableHashSet<string>
    ValidImmutableClassTypes =
        ImmutableHashSet.Create<string>(
        "Boolean",
        "Byte",
        "SByte",
        "Char",
        "Decimal",
        "Double",
        "Single",
        "Int32",
        "UInt32",
        "Int64",
        "UInt64",
        "Int16",
        "UInt16",
        "String",
        "ImmutableArray",
        "ImmutableDictionary",
        "ImmutableList",
        "ImmutableHashSet",
        "ImmutableSortedDictionary",
        "ImmutableSortedSet",
        "ImmutableStack",
        "ImmutableQueue"
    );

    public InvalidDataTypeException(
        ImmutableHashSet<string> invalidProperties) : base(
        $"Properties of an instance of " +
        "ImmutableClass may only " +
        "contain the following types: Boolean, Byte, " +
        "SByte, Char, Decimal, Double, Single, " +
        "Int32, UInt32, Int64, " +
        "UInt64, Int16, UInt16, String, ImmutableArray, " +
        "ImmutableDictionary, ImmutableList, ImmutableQueue, " +
        "ImmutableSortedSet, ImmutableStack or ImmutableClass. " +

        $"Invalid property types: " +
        $" {string.Join(",", invalidProperties.ToArray())}")
    {
        Data.Add("InvalidPropertyTypes",
            invalidProperties.ToArray());
    }
}

See also

Learn the basics on immutability

init keyword C# 9

Init only setters provide consistent syntax to initialize members of an object. Property initializers make it clear which value is setting which property. The downside is that those properties must be settable. Starting with C# 9.0, you can create init accessors instead of set accessors for properties and indexers. Callers can use property initializer syntax to set these values in creation expressions, but those properties are readonly once construction has completed. Init only setters provide a window to change state. That window closes when the construction phase ends. The construction phase effectively ends after all initialization, including property initializers and with-expressions have completed.

Common classes

init-only setter

The main differences and similarities are described in this table:

Constructor parameter init property
Since C# 1.0 C# 9.0
Are required / Has hard guarantees about being present Yes No
Self-documenting Since C# 4.0 Yes
Can overwrite readonly fields Yes Yes
Suitable for Required and optional values Optional values
Ease of reflection Just use ConstructorInfo Horrible
Supported by MEDI Yes No
Breaks IDisposable No Yes
Class knows init order Yes No
Ergonomics when subclassing Tedious Decent

The downsides of init are mostly inherited from the downsides of C#'s object-initializer expressions which still have numerous issues (in the footnote).

As for when you should vs. shouldn't:

  • Don't use init properties for required values - use a constructor parameter instead.
  • Do use init properties for nonessential, non-required, or otherwise optional values that when set via individual properties do not invalidate the object's state.

  • In short, init properties make it slightly easier to initialize nonessential properties in immutable types - however they also make it easier to shoot yourself in the foot if you're using init for required members instead of using a constructor parameter, especially C# 8.0 nullable-reference-types (as there's no guarantees that a non-nullable reference-type property will ever be assigned).
  • In terms of guidance:
    • If your class is not immutable, or at least does not employ immutable-semantics on certain properties then you don't need to use init on those properties.
    • If it's a struct then don't use init properties at all, due to all the small details in struct copy behavior.
    • In my opinion (not shared by everyone else), I recommend you consider an optional (could also be nullable) constructor parameter or an entire different constructor overload instead of init properties given the problems I feel they have and lack of any real advantages.

Footnote: Problems with C# object-initializer syntax, inherited by init properties:

  • Breaks debugging: Even in C# 9, if any line of the initializer throws an exception then the exception's StackTrace will be the same line as the new statement instead of the line of the sub-expression that caused the exception.
  • Breaks IDisposable: if a property-setter (or initialization expression) throws an exception and if the type implements IDisposable then the newly created instance will not be disposed-of, even though the constructor completed (and the object is fully initialized as far as the CLR is concerned).

Struct

In .NET, there are two categories of types, reference types and value types.

Structs are value types and classes are reference types.

The general difference is that a reference type lives on the heap, and a value type lives inline, that is, wherever it is your variable or field is defined.

A variable containing a value type contains the entire value type value. For a struct, that means that the variable contains the entire struct, with all its fields.

A variable containing a reference type contains a pointer, or a reference to somewhere else in memory where the actual value resides.

This has one benefit, to begin with:

  • value types always contains a value
  • reference types can contain a null-reference, meaning that they don't refer to anything at all at the moment

Internally, reference types are implemented as pointers, and knowing that, and knowing how variable assignment works, there are other behavioral patterns:

  • Copying the contents of a value type variable into another variable, copies the entire contents into the new variable, making the two distinct. In other words, after the copy, changes to one won't affect the other
  • Copying the contents of a reference type variable into another variable, copies the reference, which means you now have two references to the same somewhere else storage of the actual data. In other words, after the copy, changing the data in one reference will appear to affect the other as well, but only because you're really just looking at the same data both places

Difference between Structs and Classes:

img

img

Anonymous Types

Anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without having to explicitly define a type first. The type name is generated by the compiler and is not available at the source code level. The type of each property is inferred by the compiler.

You create anonymous types by using the new operator together with an object initializer.

Example

var contacts = new[]
{
    new
    {
        Name = "Eugene Jones",
        PhoneNumbers = new[] { "206-555-0108", "425-555-0001" }
    },
    new
    {
        Name = "Mary Smith",
        PhoneNumbers = new[] { "650-555-0199" }
    }
};

foreach (var contact in contacts)
{
    Debug.WriteLine($"{contact.Name}");
    foreach (var phoneNumber in contact.PhoneNumbers)
    {
        Debug.WriteLine($"\t{phoneNumber}");
    }
}

While for a broad scope

SingleContact[] contacts =
{
    new()
    {
        Name = "Eugene Jones",
        PhoneNumbers = new[] { "206-555-0108", "425-555-0001" }
    },
    new()
    {
        Name = "Mary Smith",
        PhoneNumbers = new[] { "650-555-0199" }
    }
};

foreach (var contact in contacts)
{
    Debug.WriteLine($"{contact.Name}");
    foreach (var phoneNumber in contact.PhoneNumbers)
    {
        Debug.WriteLine($"\t{phoneNumber}");
    }
}

Pluralsight courses

Advanced C# Collections (four hours)

Immutability repository

Learn the basics on immutability

Other

Working with multiple environments for connection strings

About

For teaching collections and similar topics


Languages

Language:TSQL 82.7%Language:C# 17.3%