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
vsmutable
- 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 aDataTable
- Container allows modications to data that should not be modified, meaning an
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.
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());
}
}
Learn the basics on immutability
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
- ImmutableList<T> Class
- ImmutableDictionary<TKey,TValue>
- ImmutableHashSet<T>
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 usinginit
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 useinit
on those properties. - If it's a
struct
then don't useinit
properties at all, due to all the small details instruct
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.
- If your
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 thenew
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 implementsIDisposable
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).
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 valuereference 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
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.
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}");
}
}
Advanced C# Collections (four hours)