You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
csharp-basics/13. Generics, IEnumerable a...

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!
  • 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 property Country
  • 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 a foreach loop in that it holds a copy of the current element in the persons 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;

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() or ToList() 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!