C# List – The ultimate Guide in 2024 – 15 years of .NET

C# List – The ultimate Guide
C# List – The ultimate Guide

The C# list is more than just a typical „group of things“. According to it’s official documentation it’s an object consisting of strongly typed objects. The objects inside of lists can be accessed by something called index, which is some sort of common enumeration / numbering style in programming. Additionally, C# lists can be manipulated by using some typical helper methods like „Add“, „RemoveAt“ or even sorted, by like „Sort“.

The C# List – more than just a group of objects

Before we start our deep dive into the technical world of C# Lists, we will first start to evaluate on the concept itself. I mean, how would we be fully able to understand, what’s going on, if we don’t understand the problems it’s trying to solve.

Another thing is, that we will learn to think about common pitfalls, like when we should rather not be using the list type. Many people will just always fall back to a C# List, when there would be better opportunities at hand. I’m talking about usability or even considering performance. As professional developers, we should of course aim for professional software, right!?

infoThis guide aims for helping you, getting a deep understanding about the C# List. If you however just need some quick examples, feel free to visit the corresponding section. You can of course use the table of contents as well.

But no worries, I will guide you through all of that through this post! As always: If you have any suggestions for improvements or like general questions, feel free to talk about them in the comments section further down.

What are Lists in C# or .NET in general?

What is a C# list and what is its use case?
What is a C# list and what is its use case?

When starting to think about a C# list (or .NET in general, like VB.NET) you can imagine some sort of related / closely coupled objects. Just like in the real life, we can „list up“ things together, so we can like „group“ them. You could say, that their togetherness has a special meaning.

For the first actual – and easy – code example, think about simple numbers like for example: 3, 5, 7 and 9. These numbers alone are pretty non-saying, right? I mean of course, a mathematician would probably see those in a special way, okay, but not everybody. This is where the list could come into play!

Take a first look at those numbers standing „alone“. You could possibly think of: „Well Robert, I could just name them in a related style..“. Of course you could, but have fun, doing this with like 30, 50 or even 100 variables – please don’t do that! Anyways, here’s the promised example:

// some numbers on their own
// without much sense
int numberOne = 3;
int numberTwo = 5;
int numberThree = 7;
int numberFour = 9;

We could now for example give those numbers a special meaning and make it clear, that they actually belong together. We will do this by – you guessed right – using a C# List object! Take a look at the soon following code, where I’ll create a list with the typical pattern for initializing variables.

Thinking about the „sub type“

But before that, we need to actually think of the type itself! I mean, yes, the „List“ in itself is a type, but it’s a so called generic type (at least if we are using the List type defined in the System.Collections.Generic namespace, which is probably the most used one).

Lemme explain this a bit better: Usually you would have to create a class for each list type, like: „A list of numbers“, „a list of strings“ or even „a list of football players“. This would pretty much suck, right? I mean the work that would have to be done, would be pretty aweful.

It could look like this – horrible, right?:

// BAD CODE, DON'T USE
// just for explanation purposes

class ListOfNumbers
{
  // some implementation stuff
}

class ListOfStrings
{
  // some implementation stuff
}

class ListOfPlayers
{
  // some implementation stuff
}

But thanks to the .NET framework, we have another much easier approach. An approach that avoids exactly this problem of creating a custom for each object-type over and over again. Let’s take a look at that in the next section.

Generic C# Lists to the rescue!

Generic C# lists help avoiding code duplication and work
Generic C# lists help avoiding code duplication and work

In the last step, we took a look of the first problem, we could possibly encounter in our thinking process considering lists: The creation of a potential sub-type for each list. As we already noticed, this would suck, because we would have so much additional and repetitive work to do! Let’s now see how the .NET framework handles this problem for us.

The solution is called „generics“, this basically means, that we don’t need a special sub type for each listing type. I’m talking about like listing ints, strings, customers and so on. We can actually use some sort of a more broad solution approach – more „generic“, so to say, heh, get it!?

We could even dive deeper on those „avoid repetitive code“ and „no hell of additional work“ aspects. Again, I don’t want to leave you standing there with „this is how I add entries“ and „this is how I delete entries“. This guide is about deep understanding of lists and crossing topics!

How do we create generic approaches?

Looking deeper into the „generic“ stuff, let’s make a very basic example, before we analyse the list itself. For this, I will take a typical „Id“ thingy as starting point. For the sake of simplicity, I will omit any big methods, just focusing on like a single property or small method, etc. Due to the same reason, I won’t be using Task / async stuff as well – just a quick example.

Imagine you have something (mostly called Repository) that should store some other thing (like a Customer) into a database. I mean, it’s actually a pretty „real-world-ish“ example, to be honest. For that, you could start to create a usual class like the following:

class Customer { /* some stuff... */ }

class CustomerRepository
{

  public void Create(Customer customer)
  {
    // persist to storage somehow..
  }

}

And use it that way:

// initialize
var customer = new Customer();
var customerRepository = new CustomerRepository();

// use it
customerRepository.Create(customer);

Just like in one of the last explained problematic situations, the same problem arises here, again. You would have to do the same stuff over and over again, creating similar classes for bills or whatever. Of course, you could try to use some sort of base class, but this wouldn’t be as strongly typed as the generic approach. Again, I’m trying to explain it as easy as possible, avoiding the need to dive into like Interfaces, Abstract Classes, etc.

Improved generic version

Let’s now look at a more generic, more reusable approach. Keep an eye on that „T“-stuff, as this marks the class as generic one. As I started learning about generics myself, I always tried to remind me of reading the „T“ as „of“. This results in descriptions like „This is a repository of / for customers“.

class CustomerEntity { /* some stuff.. */ }

class Repository<T>
{

  public virtual void Create(T item)
  {
    // some default db logic
    // to process the item
    // could be overriden, cuz it's virtual
  }

}

class CustomerRepository : Repository<CustomerEntity>
{

  // we could either override that method
  // or leave it as it is in the base class
  public override void Create(CustomerEntity item)
  {
    // some other logic to process
    // the now strongly typed CustomerEntity
  }

  // more methods

}

I know, that this could potentially be confusing, especially for beginners, but believe me, the generic List class is much more complicated. I mean, nobody can hold us back from looking at the .NET’s implementation of the list class.

Just visit the official Github code from Microsoft regarding the List, if you really want to be scared 🤷‍♂️! But don’t blame me afterwards, haha – this example here is much easier to study!

But what about Arrays ’n‘ stuff?

Using C# lists - What about Arrays?
Using C# lists – What about Arrays?

If you now think about like „…but what about Arrays? And are there other list-ish types?“ – I have to say: Perfectly valid questions! I mean, one of the first datatypes you learn about, when starting programming, is an Array, right!?

Two main problems with Arrays

The problem with Arrays is pretty simple: You need to know the length of the Array during initialization. Therefore adding and removing items is pretty much a pain in the …, you know? Not only usability wise, it’s also pretty expensive from an allocation side of view.

Another problem is, that you don’t have the beautiful and easy methods, a list would provide to you:

Add Remove RemoveAt not available on C# Arrays
Add Remove RemoveAt not available on C# Arrays

I mean there are certain situations, where an Array would be the better approach. Do we really want to go down the rabbit hole this far, right now? I mean, we would be for example talking about buffers, (re-) allocations and more. I think, this is too much at this moment, so maybe I’ll create another post for this, later.

What about other types?

If you are looking for other types which are able to store some sort of a „list“ or a close relative to that – oh boy – there are many. Choosing the right stuff, for the right situation needs some experience and or training. As a beginner, you should probably remember to just use a list, when it comes to „I want to group / list things together“.

Please – of course – feel free to read into the other possibilities as well. I will try to do more stuff on that in the feature, by like writing a blog post, but well, time :).

Next, there will be some common other types, you can use, to represent something like a „list“ – be careful on their use cases though. In the next section, I will at least explain one important other type, which is a perfect example of „When not to use lists“.

But here’s your list, borrowed from the official Microsoft documentation about „Commonly used collection types“ – please don’t overload yourself and focus on the List, if you’re a beginner.

List types only containing a „value“:

Other list types containing a „key“ and a „value“ (pair):

In the next (important) section, we will talk about when to actually NOT use the list.

When not to use Lists & alternatives instead!

When not to use the C# list – and alternatives that are better in some scenarios!
When not to use the C# list – and alternatives that are better in some scenarios!

So, as this post is pretty much growing and growing, I’ll try to be as short and easy as I can be here. Without losing too much important stuff of course. As alread mentioned somewhere above, the most persons will do the following decision: „Well, I have not one element, but potentially many, so I’m going for a list – bang“.

But is this actually the right way? I mean, I kinda spoilered it in the heading… No, this decision isn’t always good! It can actually harm your application performance very critically. But again – as a learner / beginner, you can possibly go with that till a certain point.

Keep in mind though, that this is only one out of some examples, where you shouldn’t be using a typical generic C# list.

A real world example – a phonebook

As I pretty much enjoy real life coupled examples, the next one won’t be an exclusion. Imagine you’re acting as Google, therefore getting requests and telling answers to those. I want you to especially think about something like a phonebook.

Someone can therefore come to you, ask for like: „Hey, can you please tell me Robert’s phone number?“. And then you could respond like: „Of course, it’s xyz“. When you’re asked about like 3-5 persons, it’s a pretty much doable task, but what if that count grows to like 10 or 15? What about 100?

Small amounts could be doable, using a small helper „list“ like this one:

A typical scenario when you shouldnt use a List in C#
A typical scenario when you shouldnt use a List in C#

In the next step, we will recreate the approach using C#.

Creating the phonebook approach

Demonstrating the „How not to do it / when to avoid lists“, we will now create the stuff from above, in an object orientied way using C#. At first, we have entries inside of our phonebook, therefore called „PhoneBookEntry“.

Here’s the class for that:

// BAD CODE (at least soon)
class PhoneBookEntry
{

   public string Name { get; set; }

   public string PhoneNumber { get; set; }

   public PhoneBookEntry(string name, string phoneNumber)
   {
      Name = name;
      PhoneNumber = phoneNumber;
   }

}

We can now can create a simple phonebook, by „grouping“ entries together to a, well, List called phonebook. I mean, I could use a loop with integers as phonenumbers for displaying purposes as well, but – I did it like this now:

// BAD CODE (don't do it this way)
// just for explanation...
var phoneBook = new List<PhoneBookEntry>();
phoneBook.Add(new PhoneBookEntry("Robert", "<somenumber>"));
phoneBook.Add(new PhoneBookEntry("Anne", "..."));
phoneBook.Add(new PhoneBookEntry("Bob", "..."));
phoneBook.Add(new PhoneBookEntry("Carl", "..."));
phoneBook.Add(new PhoneBookEntry("Kevin", "..."));

// you get it...

Finding the phone number for a person

The bad thing will come now, even though it – of course – „works“. We will now fetch a phone number by a provided name, like this:

// BADE CODE (don't do it this way)
// just for explanation...
private string GetPhoneNumberByName(string name, List<PhoneBookEntry> phoneBook)
{
  // iterate each entry - SPOILER, THIS is the problem!
  foreach (var entry in phoneBook) {
    // if we found a match
    if (entry.Name == name) {
      // return the corresponding phoneNumber
      return entry.PhoneNumber;
    }
  }
  // if not found..
  return "";
}

We can now call it like this:

// BADE CODE (don't do it this way)
// just for explanation...

// putting the last two code sections
// together and then somewhere executable:

// find roberts number
var robertPhoneNumber = GetPhoneNumberByName("Robert", phoneBook);

Great, we constructed some working code, but sadly, it’s bad! Why though!? This will be explained in the next sub-section.

Why it’s bad this way?

We will now take a look at why our previous approach is actually pretty bad. With a small amount of data, you won’t pretty much notice a big problem. But as soon as the data volume grows, you will.

Experienced developers will of course spot the problem in an instant – due to experience! If you’re a beginner, don’t let this put you down, you’re here to learn, right!? Again, I will explain this with expanding the real life example we started above.

Imagine you need to „crawl“ through this little list, when someone asks you for a persons number. This is no big deal, when the list is small, right? Think about what will happen, when this list actually contains 100, 1000 or even – holy sh** – 10000 entries? I think you would probably (and understandably) cry in desperation!

Even though that computers work pretty differently than humans, in this case, you made them working very much the same. For each entry that would be added in this list, your needed work to find the specific phone number could grow. Why the „could“, you ask?

Well, we could be lucky to find our target number in the first entry. But we could also be very unlucky to find our target number pretty far at the end. The situation from above is usually called a „complexity of O(n)“.

How to do it then?

Back to the real world again, wouldn’t it be amazing, if you could just be able to open the target entry? I mean, without searching through possibly thousands of entries after another? Yes, right? And we can do that – I mean, at least the computer in this case, regarding C#.

For this, we will use a more fitting data type. When you think about it, we have a key to value association. That means, each name is „mapped“ to one phone number. I mean, we could effortlessly do „one name, multiple phone numbers“ but let’s keep the „one number“ approach.

Having the situation identified – being a key to value structure – we will just use the fitting datatype called „Dictionary“ in C#. You can add entries to a Dictionary and retrieve it’s values by providing the key – nice!

Let’s now fix the code from above using a dictionary:

var betterPhoneBook = new Dictionary<string, string>();
betterPhoneBook.Add("Robert", "<somenumber>");
betterPhoneBook.Add("Anne", "...");
betterPhoneBook.Add("Bob", "...");

// retrieving roberts number, but better!
var robertsPhoneNumber = betterPhoneBook["Robert"];

We now solved the problem of being in a O(n) complexity and improved it now being a O(1) operation. The retrieval time of someones phone number should now be pretty much the same. No matter if there are 10 or 10000 numbers. And we won’t just take it like that, we will actually test that by stopping the time!

The proof – example application

Creating some sort of proof considering the last section, lemme show you some quick screenshots of the tests. I will create a dataset of 300000 entries using our first (List) and our second (Dictionary) approach. I’ll then measure the access times and display them. To make it easier for me, I used one of the typical „run dotnet code online“ sites.

Here’s the result for the C# List based approach:

Time needed for a 300k dataset list based search

This is the result for the Dictionary based approach – and holy sh**, this is devastating:

Time needed for a 300k dataset dictionary based search

After those examples, the „Why you shouldn’t use a C# List in this type of requirement“ should be pretty much self explanatory!

C# List – Quick examples

C# List – Quick Examples
C# List – Quick Examples

If the above sections are too like „big“ and you just need some sort of quick examples: I got you! Please have a look at this collection of quick and easy examples for the C# list. Feel free to use the table of contents for faster navigation though the examples.

Creating lists

With type inference (using var)

// creates a list of integers
var myList = new List<int>();

// use it..

The usual way

List<int> myList = new List<int>();

Only declaring, not instantiating

// should use nullable nowadays!
List<int>? myList = null;

Instantiating later

// should use nullable nowadays!
List<int>? myList = null;

// after some time.. whatever..
myList = new List<int>();

// check if null, before usage
// if (myList == null)...
// if (myList != null)...

With values

var names = new List<string>()
{
  "Robert",
  "Anne",
  "Bob"
};

With array initialization syntax (C# >= 12)

List<string> names = ["Robert", "Anne", "Bob"];

Adding items

Adding simple items to a list

// simple list of integers
var myList = new List<int>();
// adding one item
myList.Add(5);
// adding another one
myList.Add(7);

Adding more complex objects to a list

// the class being used, for the objects
class Customer { /* some customer stuff */ }

// a list of objects
// of course you can use the explicit type as well
// / without using type inference
// like List<Customer> customers...
var customers = new List<Customer>();

// add a customer
customers.Add(new Customer());

// add another customer
customers.Add(new Customer());

Adding different sub-objects to a list

// base class
abstract class Person { /* some implementation details */ }

// one derivate
class Employee { /* ... */ }

// another sub-type
class Boss { /* ... */ }

// the common list (this time explicitly defining the generic type)
List<Person> companyMembers = new List<Person>();

// adding a boss - being a person
companyMembers.Add(new Boss());

// adding an employee - being a person
companyMembers.Add(new Employee());

Removing items

Removing a specific „element“

// example with strings
var names = new List<string>()
{
  "Robert",
  "Anne",
  "Bob"
};

// removing "Anne"
names.Remove("Anne");

// example with objects
var customerOne = new Customer();
var customerTwo = new Customer();
var customers = new List<Customer>()
{
  customerOne,
  customerTwo
};

// remove the second customer
customers.Remove(customerTwo);

Removing via index / known location

var myList = new List<string>()
{
  "some",
  "example",
  "strings"
};

// delete the string at index 1
// (the second position)
myList.RemoveAt(1);

Removing the last element

var myList = new List<string>()
{
  "some",
  "example",
  "strings"
};

// check for items to avoid exception
if (myList.Length > 0)
{
  // delete the last item
  myList.RemoveAt(myList.Length - 1);
}

Conclusion

At the end of this post, we’ve talked about many aspects of one of the most used datatypes / classes in the .NET framework: The (generic) List type. We learned about the „why“ and the „how“ and dived into more detailed explanations behind. In contrary to most „tutorials“, we took a look at „When NOT to use the list“ cases, as well. Hopefully this post contained every information you needed.

Related posts

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert