We've worked with loops, conditions, methods, and our own types in C# but what about collections of objects? A group of Person objects or Products that are added to a shopping cart, how do we handle those?
There are a number of different collection objects that you can use that implement the same basic interactions.
Arrays are reference types and the simplest of the collection types, and can be declared with one to many dimensions and can also be declared jagged. Simplify declared with a type and square brackets [ ]
defining the size of the array, initialized with a new
statement and curly braces { }
optionally containing the initial values of the array.
int[] numbers;
// Numbers doesn't contain anything, as it wasn't assigned yet
display("Array is created: " + (numbers == null).ToString());
// Create an array by using square brackets containing a size
numbers = new int[3];
display("Array is null: " + (numbers == null).ToString());
// The read-only Length property shows the number of elements in the array
display("Array Size: " + numbers.Length);
// Declare the array with initial values
var fullArrayOfNumbers = new int[3] {1, 2, 3};
display("Array Size: " + fullArrayOfNumbers.Length);
Array is created: True
Array is null: False
Array Size: 3
Array Size: 3
You can then interact with the values of the array using numeric a numeric indexer starting with a base value of 0
display("Item[0]: " + fullArrayOfNumbers[0]);
// You can set values on the array using the equals assignment operator
fullArrayOfNumbers[0] = 5;
display("Item[0]: " + fullArrayOfNumbers[0]);
Item[0]: 1
Item[0]: 5
// You cannot interact with array values outside the size of the array
// display(fullArrayOfNumbers[5]);
fullArrayOfNumbers[5] = 100;
System.IndexOutOfRangeException: Index was outside the bounds of the array. at Submission#6.<<Initialize>>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken) at Submission#6.<<Initialize>>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
// You can work with multi-dimensional arrays as well
var matrix = new int[3,2] { {1,2}, {3,4}, {5,6} };
display(matrix.Length);
// Access elements of the multi-dimensional using a comma between index values
matrix[0,1]
The challenge with arrays is that you cannot easily add or remove objects from the array without going through a complex bit of resizing using the Array.Resize
method.
var myNumbers = new int[] {1,2,3};
display(myNumbers);
// This doesn't work
//myNumbers.Add(4);
index | value |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
You can change the size of a one-dimensional array using the Array.Resize method. This method does not just resize the array, but rather creates a new array of the desired size and copies the values into that new array.
// This does
Array.Resize(ref myNumbers, 4);
myNumbers[3] = 4;
display(myNumbers);
index | value |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
3 | 4 |
// Capture a reference to myNumbers
var referenceToMyNumbers = myNumbers;
// Change myNumbers
Array.Resize(ref myNumbers, 5);
myNumbers[4] = 100;
display(myNumbers);
index | value |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
3 | 4 |
4 | 100 |
// Let's see what referenceToMyNumbers contains:
referenceToMyNumbers
index | value |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
3 | 4 |
referenceToMyNumbers
contains the contents of myNumbers
before the resize because it was NOT effected by the Array.Resize
operation which makes a copy of the array.
You can remove elements from the array by resizing to a smaller number of elements. This will eliminate contents from the end of the array.
// Remove is similar, and eliminates elements from the end of the array
Array.Resize(ref myNumbers, 3);
display(myNumbers);
index | value |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
Arrays are enumerable and implement the IEnumerable
interface, meaning you can iterate over the contents of a collection with a loop and interact with them:
// i in this case returns the element in the collection, not the index
foreach (var i in myNumbers) {
display(i);
}
Array.Fill
var myOneArray = new int[3];
Array.Fill(myOneArray, 1);
myOneArray
index | value |
---|---|
0 | 1 |
1 | 1 |
2 | 1 |
A Hashtable and SortedList are collections of key/value pairs that contain no duplicate keys. The Hashtable
is sorted based on the hash hash of the keys and a SortedList
is sorted based on the key value
//var fileExt = new Hashtable();
var fileExt = new SortedList();
fileExt.Add("txt", "Plain text");
fileExt.Add("mp3", "Compressed Music");
fileExt.Add("jpg", "Jpeg Compressed Images");
fileExt
key | value |
---|---|
jpg | Jpeg Compressed Images |
mp3 | Compressed Music |
txt | Plain text |
// No duplicates are allowed
//fileExt.Add("mp3", "Sound effects");
fileExt["mp3"]
Compressed Music
foreach (var kv in fileExt)
{
display(((DictionaryEntry)kv).Key);
}
jpg
mp3
txt
var myQueue = new Queue();
myQueue.Enqueue("First");
myQueue.Enqueue("Second");
myQueue.Enqueue("Third");
myQueue
index | value |
---|---|
0 | First |
1 | Second |
2 | Third |
// Use Count to check the size of the queue
myQueue.Count
// Use Peek to inspect the next value off of the queue
display(myQueue.Peek());
First
var z = myQueue.Dequeue();
z
First
A Stack is a collection that is accessed in Last-in/First-out manner using the Push
and Pop
methods to add and remove items, with the Peek
method available to examine the next item to be removed from the Stack
. I think of a Stack
like a deck of cards: the last card that is placed on the top of the deck is the first to be dealt to a player.
var myHand = new Stack();
myHand.Push("A-d");
myHand.Push("A-s");
myHand.Push("9-h");
myHand.Push("9-s");
myHand.Push("9-c");
myHand
index | value |
---|---|
0 | 9-c |
1 | 9-s |
2 | 9-h |
3 | A-s |
4 | A-d |
var myCard = myHand.Peek();
myCard // The 9-Clubs is returned first because it was Pushed onto the Stack LAST
9-c
foreach (var item in myHand)
{
display(item);
}
display(myHand.Peek());
var thisCard = myHand.Pop();
thisCard
9-c
9-s
9-h
A-s
A-d
9-c
9-c
We've looked at these basic interactions with collection types and the keys or values stored don't have a specific type associated. You can get into some hairy situations dealing with type conversions if you mix and match types for keys or values.
class Card {
public string Rank;
public string Suit;
public Card(string id) {
Rank = id.Split('-')[0];
Suit = id.Split('-')[1];
}
public override string ToString() { return Rank + "-" + Suit; }
}
var deckOfCards = new Stack();
deckOfCards.Push(new Card("A-d"));
deckOfCards.Push(new Card("K-h"));
// Now add a Joker card
deckOfCards.Push("Joker");
deckOfCards.Push(new Card("J-h"));
deckOfCards
index | type | Rank | Suit | value |
---|---|---|---|---|
0 | Submission#26+Card | J | h | |
1 | System.String | Joker | ||
2 | Submission#26+Card | K | h | |
3 | Submission#26+Card | A | d |
// take a card off the deck
var myCard = deckOfCards.Pop();
myCard
Rank | Suit |
---|---|
J | h |
Generics are a way for you to force the type of a parameter from within client code. You declare the type generically using the convention <T>
with a class name or a method name and this allows that type to be passed around and enforced on those methods or properties in a class.
Most developers are familiar with using Generic Collections, which enforce the type of the objects in the collection. You'll find a Queue<T>
and a Stack<T>
available in the System.Collections.Generic
namespace that mirror the versions we used above, as well as a few others list List<T>
and Dictionary<T>
.
Let's take a look at some examples
The List<T> is the most flexible of the generic collections, allowing you to add, remove, and access objects of the specified type. Let's take a look at that deck of cards sample again:
// Declare the list with the specified type inside angle-brackets
var listOfCards = new List<Card>();
listOfCards.Add(new Card("A-d"));
listOfCards.Add(new Card("J-d"));
listOfCards.Add(new Card("9-c"));
listOfCards.Add(new Card("8-s"));
display(listOfCards.GetType());
listOfCards
index | Rank | Suit |
---|---|---|
0 | A | d |
1 | J | d |
2 | 9 | c |
3 | 8 | s |
We can randomly access an element anywhere in the List<Card>
, similar to choosing a card from the middle of the deck:
listOfCards[2]
Rank | Suit |
---|---|
9 | c |
Similary, we can also Insert
to add a card into the middle of the deck at a specific index. Perhaps I have the three of hearts and want to insert it as the third card in the collection:
var ThreeHearts = new Card("3-h");
listOfCards.Insert(2, ThreeHearts);
listOfCards
index | Rank | Suit |
---|---|---|
0 | A | d |
1 | J | d |
2 | 3 | h |
3 | 9 | c |
4 | 8 | s |
I can ask the list where the three of hearts is located by using the IndexOf
method to locate it in the deck. I bet that sleight of hand magician could use this technique to find the three of hearts in a deck of cards. Its not magic, just C#:
listOfCards.IndexOf(ThreeHearts)
You can also ask the List<Card>
what the card is at a specific index by using the ElementAt
method
listOfCards.ElementAt(1)
Rank | Suit |
---|---|
J | d |
This list is generically typed to a Card
and we read the type List<Card>
in English as "List of type Card". Thanks to this typing, we cannot add anything that isn't a card to the listOfCards
:
//listOfCards.Add("Joker");
In some classes, you may have multiple type-arguments like the Dictionary class. In Dictionary<TKey,TValue> there are type arguments for the key and the value stored.
var exts = new Dictionary<string, string>();
exts.Add("txt", "Plain text");
exts.Add("mp3", "Compressed Music");
exts.Add("jpg", "Jpeg Compressed Images");
exts
key | value |
---|---|
txt | Plain text |
mp3 | Compressed Music |
jpg | Jpeg Compressed Images |
exts["jpg"]
Jpeg Compressed Images
//exts.Add("mp3", "Sound Effects");
//exts.Add("card", new Card("J-h"));
A Hashset
var set = new HashSet<Card>();
set.Add(new Card("J-c"));
set.Add(new Card("A-c"));
set.Add(new Card("9-d"));
var threeHearts = new Card("3-h");
set.Add(threeHearts);
display(set);
index | Rank | Suit |
---|---|---|
0 | J | c |
1 | A | c |
2 | 9 | d |
3 | 3 | h |
If we attempt to add the 3 of Hearts a second time, it doesn't actually add another card to the Hashset because the 3 of Hearts is already present:
set.Add(threeHearts);
display(set);
index | Rank | Suit |
---|---|---|
0 | J | c |
1 | A | c |
2 | 9 | d |
3 | 3 | h |
Ok, generics are cool... but how do you create your own classes or methods to work with them? Perhaps we have our own custom collection object that randomly inserts objects into the collection and we want to work with the objects generically.
The official documentation on Generics has more details about how to interact with the managed types of the class.
Get started by declaring your class and methods using the <T>
notation to indicate that this is a generic type-parameter where the characters inside the angle-brackets are identical for all methods with the same characters.
class FritzSet<T>
{
// This normally wouldn't be public scoped, but making it public for the notebook visualizer
// The <T> for this List<T> is the same type as the <T> that this class will be created with
public List<T> _Inner = new List<T>();
// The newItem parameter will be of type T, matching the same type assigned when this class is created
public void Add(T newItem)
{
var insertAt = _Inner.Count == 0 ? 0 : new Random().Next(0,_Inner.Count+1);
_Inner.Insert(insertAt, newItem);
}
}
// Let's insert some numbers into a FritzSet of type 'int'
var set = new FritzSet<int>();
set.Add(1);
set.Add(2);
set.Add(3);
set.Add(4);
set.Add(5);
set
_Inner |
---|
[ 2, 4, 1, 3, 5 ] |
// We can also create a FritzSet that holds a bunch of Card objects
var deck = new FritzSet<Card>();
deck.Add(new Card("A-d"));
deck.Add(new Card("9-d"));
deck.Add(new Card("J-h"));
deck.Add(new Card("3-c"));
deck.Add(new Card("2-s"));
deck
_Inner |
---|
[ { Submission#26+Card: Rank: 9, Suit: d }, { Submission#26+Card: Rank: A, Suit: d }, { Submission#26+Card: Rank: J, Suit: h }, { Submission#26+Card: Rank: 2, Suit: s }, { Submission#26+Card: Rank: 3, Suit: c } ] |
// you can also force a generic type of 'object' and lose that compile-time type checking
var objSet = new FritzSet<object>();
objSet.Add(new Card("J-h"));
objSet.Add("Joker");
objSet
_Inner |
---|
[ { Submission#26+Card: Rank: J, Suit: h }, Joker ] |
What type of object are each element in the object set?
display(objSet._Inner[0].GetType());
display(objSet._Inner[1].GetType());
// If we do a foreach with a var keyword, what type is emitted?
foreach (var card in objSet._Inner) {
display(card.GetType());
}
LINQ (Language Integrated Query which Fritz keeps calling 'Language Integrated Natural Query') refers to a collection of technologies that allow you to query data. We're going to start with a subset of LINQ called LINQ to Objects that allow you to add predicate methods to IEnumerable<T>
or IQueryable<T>
collections to query them. We will dig in further to discuss how LINQ and LINQ to Objects works in our next session, and in the rest of this notebook we'll explore some simple interactions that are available.
Let's look at that FritzSet<Card>
collection again and load it with some Card
s
var deck = new FritzSet<Card>();
deck.Add(new Card("A-d"));
deck.Add(new Card("A-h"));
deck.Add(new Card("A-c"));
deck.Add(new Card("A-s"));
deck.Add(new Card("9-d"));
deck.Add(new Card("J-h"));
deck.Add(new Card("3-c"));
deck.Add(new Card("2-c"));
deck.Add(new Card("7-d"));
deck.Add(new Card("6-d"));
deck.Add(new Card("5-d"));
deck.Add(new Card("4-d"));
deck.Add(new Card("4-h"));
There are a set of Standard Query Operators that we can use to analyze our collection. Let's experiment with some of those operators for the remainder of this notebook.
We can count the number of objects in the deck
collection using the Count method with an optional expression called a Lambda Expression to test each object to determine whether to count that entry.
// Display the total number of cards in the list
display(deck._Inner.Count());
// Display the number of cards that are Diamonds suit.
// Use the expression body member that tests for the Suit property to equal "d". Values that are TRUE are counted
deck._Inner.Count(c => c.Suit == "d")
You can also filter the collection by using the Where method with a similar expression body member to test each member of the collection.
display(deck._Inner.Where(c => c.Suit == "d"));
// Count the returned collection after it is filtered
display("Count of Diamonds cards: " + deck._Inner.Where(c => c.Suit == "d").Count());
index | Rank | Suit |
---|---|---|
0 | A | d |
1 | 6 | d |
2 | 9 | d |
3 | 4 | d |
4 | 5 | d |
5 | 7 | d |
Count of Diamonds cards: 6
Just like the Count
method above, you can chain together these methods to filter and return data.
// Filter to JUST the Diamonds Then count the cards with rank Ace
deck._Inner.Where(c => c.Suit == "d").Count(c => c.Rank == "A")
We can also Select to return a different shaped collection of values from the collection. In other frameworks and languages this is sometimes referred to as Map
// This returns a collection of Rank values
deck._Inner.Where(c => c.Suit == "d").Select(c => c.Rank)
index | value |
---|---|
0 | A |
1 | 6 |
2 | 9 |
3 | 4 |
4 | 5 |
5 | 7 |
We can also quantify the contents of our collection using quantifier methods like Any
to query if any element matches a test condition and All
to query if all elements match a test condition:
/// This reminds me of the card game 'Go-Fish': Do you have any Queens? No? Go-Fish
deck._Inner.Any(c => c.Rank == "Q")
// Do you have a flush, all cards are the same suit?
deck._Inner.All(c => c.Suit == "d")
We can chain together these operations to analyze and inspect the collection.
Let's take that last example and filter out the cards in the clubs and hearts suits and see if we have a flush in diamonds:
deck._Inner.Where(c => c.Suit != "c" && c.Suit != "h").All(c => c.Suit == "d") // Do you have a flush of Diamonds?
// Do you have ANY cards that aren't clubs and are not hearts?
deck._Inner.Where(c => c.Suit != "c" && c.Suit != "h").Any()
We can navigate around the collection using First, Skip, and Take methods.
n
number of elements// Get the first item in the collection
deck._Inner.First()
Rank | Suit |
---|---|
A | d |
// Skip the first item in the collection and return the rest of the collection
deck._Inner.Skip(1)
index | Rank | Suit |
---|---|---|
0 | J | h |
1 | 6 | d |
2 | 4 | h |
3 | 7 | d |
4 | A | c |
5 | A | d |
6 | 5 | d |
7 | A | s |
8 | A | h |
9 | 9 | d |
10 | 2 | c |
11 | 3 | c |
// Just like at a poker game, skip 1 card and take the next 2 cards
deck._Inner.Skip(1).Take(2)
index | Rank | Suit |
---|---|---|
0 | 3 | c |
1 | 2 | c |
Interacting with a set of data is no fun without any kind of sorting. We'll end with looking at the OrderBy, OrderByDescending, ThenBy, and ThenByDescending methods.
deck._Inner.OrderBy(c => c.Rank == "A" ? "ZZ" : c.Rank).Take(10)
index | Rank | Suit |
---|---|---|
0 | 2 | c |
1 | 3 | c |
2 | 4 | d |
3 | 4 | h |
4 | 5 | d |
5 | 6 | d |
6 | 7 | d |
7 | 9 | d |
8 | J | h |
9 | A | c |
Let's try to re-order the collection to get two aces from the list of cards.
// Order by descending value of rank, converting an Ace into a 'ZZ' for ordering so that it is the first item in the collection
deck._Inner.OrderByDescending(c => c.Rank == "A" ? "ZZ" : c.Rank)
.ThenByDescending(c => c.Suit) // Then order by the Suite
.Take(2) // Take the first two items from the collection
index | Rank | Suit |
---|---|---|
0 | A | s |
1 | A | h |