The Simplicity of the New Terse Syntax [ Collection Expressions ]

Introduction

In modern C#, collection expressions, range, and spread operators offer a concise way to work with collections such as lists, hashsets, and arrays. This article explores these new syntax features, from primary constructors through collection expressions to the range and spread operators.

Let's start with the primary constructor

The primary constructor is utilized to simplify the initialization of classes. Now, you can declare the constructor directly in the class declaration, reducing boilerplate code. Here's how its syntax looks:

public class Person(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;
}

//Initialization of class Person
List<Person> people = new()
{
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
};

Listing 1. Class Person with the primary constructor.

Collection expressions

What you're seeing in Listing 1 is the traditional way of initializing a list. Let me show you the new, modern, and simpler way to initialize a list using collection expressions.

List<Person> people =
[
     new Person("Alice", 30),
     new Person("Bob", 25),
     new Person("Charlie", 35)
];

Listing 2. Initializing a list with collection expression.

A developer with common sense would consider this a joke, right? "Whoa, Microsoft changed curly braces {..} into square bracket [..], BIG WHOOP". That's what I thought, too, until Pandora's shit box was opened. Have a look at the following syntax. It shows collection expressions on other collections. That's pretty sweet! Look at that consistency.

List<int> list = [1, 2, 3, 4, 5];
HashSet<string> set = ["apple", "banana", "cherry"];

int[] ints = [1, 2, 3, 4, 5];
int[][] jaggedArray =
[
    [1, 2, 3],
    [4, 5],
    [6, 7, 8, 9]
];

Listing 3. Initializing collections with collection expressions.

The Range Operator [..]

The range operator [2..5] is utilized to specify a range between two indices in an array or a string. It provides a concise method to slice arrays and strings without the need for explicit loops or Array.Copy methods.

Note. The startIndex is inclusive, and the endIndex is exclusive.

  • [startIndex .. endIndex]: [2..5] represents a range from startIndex to endIndex - 1.
  • [startIndex ..]: This is an open-ended range, [2..] represents a range from startIndex to the end of the collection.
  • [.. endIndex]: This is even an open-ended range; [..5] represents a range from the beginning of the collection to endIndex - 1.
  • [ .. ]: This represents the entire collection.
  • ^: This operator allows counting from the end of a collection.

Let me walk you through examples of each to give you a better understanding.

Let's start with the first one,

[startIndex..endIndex] - [2..5]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[2..5]; //Output: 3, 4, 5
  • Start Index: The start index is 2. This corresponds to the element at index 2, which is 3.
  • End Index: The end index is 5. This corresponds to the element at index 5, which is 6.
  • The range [2..5] translates to elements from index 2 to index 4 (remember, the end index is exclusive), given that the output will be 3, 4, 5.

[startIndex ..] - [2..]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[2..]; //Output: 3, 4, 5, 6
  • Start Index: The start index is 2. This corresponds to the element at index 2, which is 3.
  • End Index: There is no end index specified, so it defaults to the end of the array.
  • The range [2..] translates to elements from index 2 to the end of the array. Hence the output will be 3, 4, 5, 6.

[startIndex..] : [..5]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[..5];//Output: 1, 2, 3, 4, 5
  • Start Index: The start index is not specified before the .., so it defaults to the beginning of the array, which is 0.
  • End Index: The end index is 5. This corresponds to the element at index 5, which is 6, but since the end index is exclusive, the element at index 5 is not included.
  • The range [..5] translates to elements from index 0 to index 4, which are 1, 2, 3, 4, and 5.

Range [..]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[..];//Output: 1, 2, 3, 4, 5, 6
  • Start Index: The start index is not specified before the .., so it defaults to the beginning of the array, which is 0.
  • End Index: The end index is not specified either after the .., so it defaults to the end of the array.
  • The range [..] translates to elements from index 0 to the end of the array. So this is an easy way to copy an array.

^1 Operator

int[] arr = [1, 2, 3, 4, 5, 6];
int result = arr[^1];//Output: 6
  • ^1 means "the first element from the end".
  • This corresponds to the element at index 5, which is 6.

Range [ .. ] with ^ - [ ^3 .. ]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[^3..];//Output 4, 5, 6
  • Start Index: The start index ^3 means "the third element from the end", which corresponds to the element 4.
  • End Index: There is no end index specified, so it defaults to the end of the array.
  • The range [^3..] translates to elements from element 4 to the end of the array. Hence the output will be 4, 5, 6

Range [ .. ] with ^ - [ .. ^3 ]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[..^3];//Output: 1, 2, 3
  • Start Index: The start index is not specified before the "..", so it defaults to the beginning of the array, which is 0.
  • End Index: Again, ^3 means "the third element from the end", which is 4. Since the end index is exclusive, the element at index 4 is not included.
  • The range [..^3] translates to elements from index 0th to the 2nd index. Hence, the output will be 1, 2, 3.

Range [ .. ] with ^ - [ ^3 .. ^1 ]

int[] arr = [1, 2, 3, 4, 5, 6];
int[] result = arr[^3..^1];//Output: 4, 5
  • Start Index: The start index ^3 means "the third element from the end", which corresponds to the element 4.
  • End Index: ^1 means "the first element from the end", which is 6. Since the end index is exclusive, hence 6 is not included.
  • The range [^3..^1] translates to elements from element 4 to element 5. Hence, the output will be 4, 5

Cloaning Arrays

int[] inputArray = [1, 2, 3, 4, 5, 6];
int[] cloneArray1 = [..inputArray]; //Output: 1, 2, 3, 4, 5, 6
int[] cloneArray2 = inputArray[..]; //Output: 1, 2, 3, 4, 5, 6

You can clone an array using either of the following methods.

  • [.. inputArray]: This syntax utilizes the range operator in a new array initializer. The range operator ".." without any indices specifies the entire array. Therefore, [..inputArray] means "create a new array and populate it with all the elements of inputArray". This effectively clones the entire input array into cloneArray1.
  • inputArray[ .. ]: This syntax inputArray[..] uses the range operator to create a slice of the array. [..] without any indices means "from the beginning to the end," which includes all elements. Therefore, inputArray[..] creates a new array containing all elements of inputArray. This also effectively clones the entire input array into cloneArray2.

Both cloneArray1 and cloneArray2 will contain the same elements as inputArray, creating clones of the original array.

Merging Arrays

int[] first = [1, 2, 3, 4, 5, 6];
int[] second = [7, 8, 9, 10];

int[] mergedArray = [..first, ..second];//output: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

int[] mergedArray = [..first, ..second]

  • This line creates a new array mergedArray.
  • It uses ..first to spread the elements of first into mergedArray.
  • It uses ..second to spread the elements of second into mergedArray.
  • The result is a new array that contains all the elements of the first, followed by all the elements of the second.

Substring Extraction with the Range Operator

string greeting = "Hello, World!";
string helloSubstring = greeting[..5]; // "Hello"
string worldSubstring = greeting[7..12]; // "World"
string worldExclamationSubstring = greeting[7..]; // "World!"

Extracting Substrings
 

greeting[..5]

  • greeting[..5] extracts characters from the beginning of the string up to index 5 (exclusive).
  • This includes characters from index 0 to 4, resulting in "Hello".

greeting[7..12]

  • greeting[7..12] extracts characters from index 7 to 11.
  • This includes characters from index 7 to 11, resulting in "World".

greeting[7..]

  • greeting[7..] extracts characters from index 7 to the end of the string.
  • This includes characters from index 7 to the end, resulting in "World!".

Sure, here's a small tip

  • When you see ".." used to specify a range between two values or indices, it's typically referred to as the "range operator".
  • When ".." is used to spread elements of an array or a collection, it's often called the "spread operator".

Conclusion

These collection expressions and slicing in C# open up a consistent way of initializing collections for developers. This code is concise and maintainable. Understanding the range operator helps a lot in substring and array operations. It might be slightly confusing at the beginning, but once you get a grip on it, it's easier than ever. Hope you enjoyed this article. See you soon.


Recommended Free Ebook
Similar Articles