14 KiB
marp | paginate | math | theme | title |
---|---|---|---|---|
true | true | mathjax | buutti | N. Generics, IEnumerable and LINQ |
Generics, IEnumerable and LINQ
Generics
- When using lists, you have to define the type of data that will be stored in the list, inside the angled brackets:
List<int> numberList = new List<int>(); // This list stores variables of type int
- This means that list is a generic class: it can contain data of any type
- Classes, structs, interfaces and methods can also be generic
- All the variables that are defined generic inside a generic container will be assigned a type only when the containing object/method is called
Creating a generic class
class GenericClassExample<T>
{
public T value; // This value will be whatever type is specified at instantiation
public void PrintTypeAndValue()
{
Console.WriteLine
($"This class contains a variable of type {value.GetType()} and of value {value}");
}
}
class Program
{
static void Main(string[] args)
{
GenericClassExample<int> example = new GenericClassExample<int>();
example.value = 20;
example.PrintTypeAndValue();
}
}
Multiple type parameters
Generic classes can receive multiple types as parameters:
class CustomContainer<T1, T2, T3>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public T3 Third { get; set; }
}
class Program
{
static void Main(string[] args)
{
CustomContainer<int, string, DateTime> container
= new CustomContainer<int, string, DateTime>();
container.First = 10;
container.Second = "Testing.";
container.Third = DateTime.Now;
}
}
Creating a generic Method
void GenericMethodExample<T>(T value)
{
Console.WriteLine
($"This method was passed a variable of type {value.GetType()} and of value {value}.");
}
GenericMethodExample<string>("ABC");
- Note: You could name the generic type as anything, e.g.
<GenericType>
. It is named<T>
by convention.
Exercise 1: Initializing a populated list
Create a generic method GetPopulatedList<T>
which takes two parameters: T value
and int length
, and returns a new list of type T
which is populated with the value
variables and has a length of length
.
Test your method out with a couple of different types and lengths:
List<string> list = GetPopulatedList<string>("Hello, there", 10);
foreach(string value in list)
{
Console.WriteLine(value);
}
IEnumerable
- Lists and arrays are both collections that implement the
IEnumerable
interface - All objects that implement the IEnumerable interface can be iterated with the foreach statement
IEnumerable<string> names = new string[] {"Harry", "Luke", "Harley"}; IEnumerable<string> days = new List<string> {"Sunday", "Monday", "Friday"}; foreach (string name in names) Console.WriteLine(name); foreach (string day in days) Console.WriteLine(day);
- The IEnumerable interface itself doesn't hold much functionality:
- However, the LINQ library includes all the methods you would typically need to apply to IEnumerables, such as filtering
LINQ
The problem
-
Here's an example of a common query a programmer might have to do:
-
Try to find an object with a certain id from an array
IEnumerable<User> userArray = new User[2] // Initialize a new array of users { new User { Id = 0, Name = "Rene" } , new User { Id = 1, Name = "Ville" } }; User FindObjectWithId(int id) { foreach (User user in userArray) if (user.Id == id) return user; return null; } Console.WriteLine(FindObjectWithId(1).Name); // Outputs "Ville"
- Having to write your own method for every possible query operation (select, filter, sort…) would be nonsensical
- The
List
class includes some methods for manipulation, but...- These only work on lists; not all
IEnumerables
(e.g. arrays) contain those methods!
- These only work on lists; not all
- Not to mention different data types altogether (objects, SQL databases, XML, JSON…)
- What to do?
The solution
- To introduce extensive query capabilities to all collection types, Language-Integrated Query (LINQ) was created
- LINQ supports querying of objects and even XML and SQL data, directly in your code
- Get started by adding the System.Linq namespace to your project:
using System.Linq;
-
Here's a solution to the earlier example that uses LINQ:
IEnumerable<User> userArray = new User[2] // Initialize a new array of users { new User { Id = 0, Name = "Rene" } , new User { Id = 1, Name = "Ville"} }; Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville"
-
The Enumerable.First method returns the first result that satisfies the expression in the parameters
-
The arrow syntax above is called a lambda expression
Lambda expressions
- Lambda expressions are a quick way of writing one-line methods
- The
First
method of LINQ takes a delegate as a parameter - We could just declare a method beforehand, and then refer to it in
First
like this:bool GetUserWithId(User user) { return user.Id == 1; } Console.WriteLine(userList.First(GetUserWithId).Name); // Outputs "Ville"
- To shorten the expression, we could use a delegate inside the parameters to refer to an anonymous method:
Console.WriteLine(userList.First(delegate (User user) { return user.Id == 1; })); // Outputs "Ville"
Lambda expressions compared
Using LINQs "First" -query with...
...a method:
bool GetUserWithId(User user)
{
return user.Id == 1;
}
Console.WriteLine(
userList.First(
GetUserWithId).Name);
// Outputs "Ville"
...an anonymous method:
Console.WriteLine(userList.First(
delegate (User user)
{
return user.Id == 1;
}));
// Outputs "Ville"
...a lambda expression:
Console.WriteLine(userList.First(
user =>
user.Id == 1).Name);
// Outputs "Ville"
Lambda expressions: An example
Regular methods can also be declared using the arrow function
static void Main(string[] args)
{
// Method body assigned with lambda expression
string PrintCheckUpper(bool upper, string text) =>
upper ? text.ToUpper() : text;
PrintCheckUpper(true, "I'm not angry!"); // Outputs I'M NOT ANGRY!
PrintCheckUpper(false, "I'm not angry!"); // Outputs I'm not angry!
}
LINQ and Lambda expressions
- Going back to our LINQ example...
IEnumerable<User> userArray = new User[2] // Initialize a new array of users { new User { Id = 0, Name = "Rene" } , new User { Id = 1, Name = "Ville"} };
- Here's a solution without lambda expressions:
bool GetUserWithId(User user) { return user.Id == 1; } Console.WriteLine(userArray.First(GetUserWithId).Name); // Outputs "Ville"
- And here's the shorter version with lambda expression:
Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville"
LINQ methods
- LINQ contains methods for filtering, ordering, grouping, joining and selecting
- Suppose we have a class
Person
that contains a propertyCountry
- The following performs filtering to the
persons
object, returning only the persons whose country is of value "Finland":List<Person> persons = new List<Person> {/* Insert data here */}; var queryResult = persons .Where(person => person.Country == "Finland");
LINQ methods listed
Method | Example | Description |
---|---|---|
Where | persons.Where(p => p.Country == "Finland") |
Filters results based on an expression |
OrderBy | persons.OrderBy(p => p.LastName) |
Orders results based on one of its properties |
Select | persons.Select(p => $"Dr. {p.LastName}") |
Converts the enumerable into an another type based on an expression |
Method | Example | Description |
---|---|---|
Skip | persons.Skip(1) |
Skips first N elements |
Take | persons.Take(5) |
Returns N elements |
ToList | persons.ToList() |
Converts IEnumerable to a list |
ToArray | persons.ToArray() |
Converts IEnumerable to an array |
Method | Example | Description |
---|---|---|
Any | persons.Any(p => p.Country == "Finland") |
Return true if at least one element satisfies a condition |
All | persons.All(p => p.Age >= 18) |
Return true if all elements satisfy a condition |
FirstOrDefault | persons.FirstOrDefault(p => !p.Active) |
Returns the first element that satisfies a condition, or null if not found |
Count | persons.Count(p => p.FirstName == "Mauri") |
Returns the count of elements that satisfy a condition, can be left blank to count all |
More LINQ methods can be found here
LINQ: A chained example
- Suppose we have a csv file
authors.csv
in our project directory.// using System.IO; string path = @"C:\some\path\authors.csv"; var authors = File.ReadAllLines(path) .Skip(1) // Skip the first line which contains the column titles .Select(line => // Using lambda expression, return the new Author objects { var columns = line.Split(','); return new Author { Id = int.Parse(columns[0]), Name = columns[1], Description = columns[2] }; }) .ToList(); // Turn the resulting IEnumerable into a list
authors
now contains all the authors from the original csv file!
Query syntax
- An alternative way of using LINQ is with the query syntax
- The following performs filtering to a
persons
object:var queryResult = from person in persons where person.country == "Finland" select person;
- The range variable
person
works like the range variable in aforeach
loop in that it holds a copy of the current element in thepersons
variable - The
where
keyword specifies the condition for the filter
LINQ queries: An example
- Suppose we have a class City that contains a property Description
- The following would print the descriptions of all cities that have descriptions less than 100 characters long
IEnumerable Cities = new List<City> {/* Insert data here */}; var filteredResult = from city in Cities where city.Description.Length < 100 select city; foreach (City result in filteredResult) Console.WriteLine(result.Description);
Extension methods vs. query syntax
- In the previous examples, we used the query syntax of LINQ
- Both the methods and queries do pretty much the same thing
- It is up to you which syntax you want to use
- The method syntax works like any normal C## methods
- The query syntax might be more approachable to those who are familiar with SQL
var methodResult = persons
.Where(person =>
person.Country == "Finland");
var queryResult = from person in persons
where person.Country == "Finland"
select person;
- All queries listed here: https://www.tutorialsteacher.com/linq/linq-standard-query-operators
ToArray()
and ToList()
methods
- Notice that the LINQ queries return an
IEnumerable
- If you need to use arrays or lists, you need to call the
ToArray()
orToList()
methods
string[] strings = new string[]
{ "Timo", "Pekka", "Taina", "Kalle" };
string[] queryResult = strings
.Where(s => s.StartsWith('T'));
string[] strings = new string[]
{ "Timo", "Pekka", "Taina", "Kalle" };
string[] queryResult = strings
.Where(s => s.StartsWith('T'))
.ToArray();
No error!
Exercise 2: Filtering Names
- Download this file of names and add it to your project folder: https://raw.githubusercontent.com/dominictarr/random-name/master/names.txt
- Read all the contents into a string array with
File.ReadAllLines()
- Create a main loop where the user is asked for a string. Print the total number of names which contain that string.
- If there are less than 10 resulting names, print the names as well!
Exercise 3: Queries on Object Lists
- Expand on the exercise 2.
- Create a new class User with two properties, int Id and string Name
- If the number of filtered names is less than 10, create a list of Users with those names and a running Id
- Sort the list of users by the length of the Name property
- Print the names and id:s of the users in the sorted list
Going Further: Extension Methods
- Recap:
IEnumerable
itself only contains one method - How does the LINQ library suddenly add all these methods to our Enumerables?
- This is possible with extension methods!