Postfix Notation: An Interview Exercise

You are applying for your first position or for a new job. You are asked to complete a coding exercise: evaluate an expression in postfix notation. Let’s see what is postfix notation and how to evaluate postfix expressions in C#.

What is Postfix Notation?

Math and some programming languages use the infix notation. It places the operator between the two operands. And, it uses parenthesis to group operations. For example, a + b and (a + b)*c.

Unlinke infix notation, postfix notation places the operator after the two operands. And, it doesn’t use any parenthesis to group expresions. For example, a b + , and a b + c * are two expression in postfix notation.

Postfix expressions are evaluated from left to right. The expression 2 3 1 * + 9 - in postfix notation is equivalent to (2 + (3 * 1)) - 9.

Interview Question

This is your interview question: Write a C# program to evaluate a postfix mathematical expression. For example, 1 1 +, 1 1 1 + +, 1 1 + 2 3 * -.

During your technical interview, don’t rush to start coding right away. Follow the next steps:

  • Understand the problem
  • Come up with some examples. What’s the simplest case? Any edge cases?
  • Ask clarification questions. Do you need to validate your input values?
  • Think out loud your solution. Your interviewer wants to see your thought process.
  • Make assumptions on the input. Start with the simplest case and work through a complete solution.
a pile of empty dishes
To evaluate a postfix expression, we need a stack. Photo by Brooke Lark on Unsplash

Evaluate postfix expressions

To evaluate a postfix expression, you need a stack.

A stack is a pile-like data structure. Stacks support two operations: add something to the pile, push, and remove something from the pile, pop.

When evaluating a postfix notation, we use a stack to hold either values from the input or already computed values.

This is the pseudocode to evaluate a postfix expression:

  1. Create an stack
  2. Split input string
  3. If the first splitted value is a number, push it to the stack.
  4. But, if it’s an operator, pop the two operands from the stack. Do the Math operation and push the result back to the stack.
  5. Go to next splitted value and repeat
  6. Finally, return value in the stack

Let’s C#!!!

Now, head to Visual Studio or Visual Studio Code and create a new project. This is how we evaluate a postfix expression in C#.

First, let’s split the input string with the Split() method. To identify the operators and operands, we can use either string comparisons or regular expressions. Let’s use regular expressions.

Regex _operandRegex = new Regex(@"-?[0-9]+");
Regex _operatorRegex = new Regex(@"[+\-*\/]");
        
public string Evaluate(string postfixExpression)
{
    var tokens = new Stack();
    string[] rawTokens = postfixExpression.Split(' ');
    foreach (var t in rawTokens)
    {
        if (_operandRegex.IsMatch(t))
        {
            tokens.Push(t);
        }
        else if (_operatorRegex.IsMatch(t))
        {
            var t1 = tokens.Pop().ToString();
            var t2 = tokens.Pop().ToString();
            var op = t;

            var result = EvaluateSingleExpression(t2, t1, op);
            if (result != null)
            {
                tokens.Push(result);
            }
        }
    }
    if (tokens.Count > 0)
    {
       return tokens.Pop().ToString();
    }

    return "";
}

private static string EvaluateSingleExpression(string value1, string value2, string op)
{
    var operand1 = Convert.ToDouble(value1);
    var operand2 = Convert.ToDouble(value2);

    if (op == "+")
    {
        var result = operand1 + operand2;
        return Convert.ToString(result);
    }
    // Similar logic for other operators

    return null;
}

Voila! That’s how we can evaluate postfix expressions in C#. For more tips to prepare yourself for your next interview, check my interview tips. Also, check my post on the difference between Func and Action in C# and how to solve the two-sum problem. Those are another two common interview questions.

Happy coding!

Visual Studio 2022 setup for C# (theme, settings, extensions)

Visual Studio is the de facto IDE for C#. You will spend countless hours inside Visual Studio. You are better off spending some time to customize it to boost your productivity.

My Visual Studio setup is heavily inspired by De-Cruft Visual Studio. It aims to leave more space to the text editor by removing unneeded menus and bars.

These are the theme, settings and extensions I use to be more productive with Visual Studio.

1. Theme and Layout

  • Color theme: Dark
  • Text Editor theme: Solarized Dark
  • Font: FiraCode 14pt
  • Zoom: 110%
  • Windows:
    • Left
      • Test Explorer
      • Source Control Explorer
    • Right
      • Properties
      • Solution Explorer
      • Git Changes
      • Team Explorer
    • Bottom
      • Output
      • Error List
      • C# Interactive: A C# REPL, so you don’t have to create a Console project to try things out
My Visual Studio 2022 setup for C#
My Visual Studio opened with a sample Console project

2. Settings

To change your Visual Studio settings, go to “Tools” menu and then to “Options”.

On the left pane, you will find Visual Studio settings groupped by features, languages and extensions.

In “Text Editor”, unselect Enable mouse click to perform Go to Definition, Selection Margin and Indicator margin.

Visual Studio Text Editor settings
Text Editor - General settings

Next, uncheck Disable CodeLens. Only activate Show Test Status and Show C# References.

Visual Studio CodeLens settings
Text Editor - All Languages - CodeLens settings

Next, in C# specific settings, enable Line numbers. And, hide Navigation Bar and Code Outlining.

Visual Studio C# settings
C# - General settings

Go down to Advanced settings to Add missing using directives on paste. Yeap, Visual Studio can add missing using staments when you paste some code. That’s a time saver!

Visual Studio C# - Advanced settings
C# - Advanced settings

After installing “Productivity Power Tools” extension, unselect Compress blank lines and Compress lines that do not have any alphanumeric characters. Code looks weird compressed.

Visual  Studio Productivity Power Tools settings
Productivity Power Tools settings

Speaking of extensions, after installing “VSVim” extension, use Handle all with Visual Studio. This way, we have the best of both worlds. And, we still have Control + C and Control + V.

Visual  Studio VsVim settings
VsVim settings

For shortcuts, add Ctrl+Shift+w to easily close all documents from the keyboard.

3. Extensions

  • VSVim It enables Vim keybindings in the text editor
  • Productivity Power Tools
    • Editor Guidelines: Add a Solarized Magenta guideline at column 120
    • Fixed Mixed Tabs
    • Custom Document Well
    • Match Margin
  • VSColorOutput It gives you a fancier Output tab with colors
  • SaveAllTheTime No more Ctrl + S. It saves automatically all modified files. Why this feature isn’t included in Visual Studio out-of-the-box? It’s already present in Visual Studio Code!
  • SolutionColor It colors the solution bar per project. No more editing production code without noticing it. Use Solarized theme, too. Red for Beta and Magenta for Production
  • LocateInTFS It finds the location of the selected file in the Source Control Explorer tab
  • AddNewFile It adds new files directly from the Solution Explorer. Forget about Right Click -> Add -> New Folder, to then add the file you want. You can create folders and files in from a single pop-up window. You only need to hit Shift + F2 and type the path, name and extension of your new file.
  • SemanticColorizer It colors fields, classes, methods and more. Make extension methods italics, only
  • NUnit 3 Test Adapter. A must-have
  • MappingGenerator It creates mappings from one object to another and from a list of parameters to an object. You need to initialize an object with sample values? In your tests for example? MappingGenerator does it for you!
  • CodeMaid It cleans and formats your files. It remove extra blank lines. It removes and sorts your using statements. It removes extra spaces before and after parenthesis. You got the idea!.
  • AsyncMethodNameFixer To not to forget to add the Async suffix on async method names.
  • Multiline Search and Replace No need to copy and paste your code into Notepad++ to replace multiple lines at once.
  • Line Endings Unifier Yes, it unifies the line endings of your files. You can choose the line ending you want to use in your files. Depending on the number of files in your solution, it could take a couple of seconds. But it does its job!
  • Moq.Autocomplete If you use Moq to create fakes, this extension is for you. Inside the Setup() methods, it autocompletes the parameter list of the faked method using It.IsAny<T>() for each parameter. A time saver! I use this extension along with these snippets for Moq.
  • Open Command Line: Right click on a project or solution and open a Terminal in the folder of that solution or project. Or, simply press Alt + Space.

4. Presentation mode

Fire a new instance from Visual Studio Developers Tools command prompt, using

devenv /RootSuffix Demo

It will open a separate and clean instance with no configurations or extensions. Any changes made to this instance won’t affect the “normal” instance next time you open Visual Studio.

My Visual Studio 2019 setup in Presentation Mode
My Visual Studio in Presentation mode opened with a sample Console project

To make Visual Studio work in Presentation mode:

  • Remove Navigation Outline, Server Explorer, Toolbox, Git changes, and Properties. Only keep Solution Explorer.
  • Disable CodeLens.
  • Use Cascadia Mono, 14pt.
  • Change Output and Developer Powershell font to Consolas 14pt.
  • Increase size of tooltips:
    • Change Statement Completion font size to 12pt.
    • Change Editor Tooltip font size to 13pt.
  • Change text highlight color to yellow.
  • Use 120% as default zoom level.
  • Install SaveAllTheTime.
  • Optionally install the GitHub theme.
  • Optionally install the MarkdownEditor extension to present using markdown files.

Voilà! That’s how I use Visual Studio 2019 for C# coding. If you’re wondering what’s Vim and why you should learn it, check my post Learning Vim for Fun and Profit.

For more productivity tools and tricks, check these programs that saved me 100 hours and how I used a Visual Studio extension and a Git feature to get rid of two recurrent review comments.

Happy coding!

What the Func, Action? Func vs Action

What’s the difference between Func and Action in C#? This is a common C# interview question. Let’s find it out!

The difference between Func and Action is the return type of the method they point to. Action references a method with no return type. And, Func references a method with a return type.

What are delegates?

It all starts with delegates.

A delegate is a pointer to a method with some input parameters and possibly a return type.

In other words, a delegate is a variable that references a method that has a given signature. Both, Func and Action are built-in delegate types.

Delegates are helpful when working with higher-order functions. This is, functions that take functions as parameter or return another function. For example, JavaScript callbacks and Python decorators are high-order functions.

Now that it’s clear what delegates are, let’s see some Func and Action declarations. For example,

  • Action<Employee> holds a void method that receives Employee as parameter.
  • Action, a void method without any parameters.
  • Func<Employee, string> represents a method that receives an Employee and returns a string.
  • Func<string> doesn’t have any parameters and returns string.

When declaring Func references, the last type inside <> tells the return type of the method being referenced. For Action references, since they don’t have a return type, the types inside <> show the input parameter types.

What the Func, Action?
Let's get Funcy. Photo by Greyson Joralemon on Unsplash

How to use Func and Action in a method?

You have already used Func, if you have used LINQ. But, in general, we use them as lambda expressions.

A lambda expression is a shorthand notation to write a method without a name, only with the body and the parameter list.

For example, let’s find the employees who have worked for more than ten years.

var allEmployees = new List<Employee> { /* Some employees here */ };

Func<Employee, bool> p = (t) => t.YearsWorked >= 10;
allEmployees.Where(p);

Or just simply

allEmployees.Where(t => t.YearsWorked >= 10);

How to declare a method that receives Func or Action?

To a declare a method that uses Func or Action as an input parameter, you have to use them like regular paramaters. Then, you have to either call Invoke on it or put parenthesis next to the name passing the appropiate parameters.

Let’s see an example of a method that uses Func.

public Employee DoSomething(Func<Employee, string> func)
{
    var employee = new Employee();
    
    // var result = func.Invoke(employee);
    // Or simply
    var result = func(employee);
    //           ^^^^^
    
    // Do something with the result here

    return employee;
}

A real-world example

Func and Action are great as small factory methods. They can be used in helper or utility methods to separete business logic from generic code.

Let’s see Func in action! Here is an example of Func from Insight.Database to create a ReliableConnection, a database connection that automatically retries on certain errors.

The ExecuteWithRetry method retries things and uses Func for the operation to retry. Some of the code has been removed for brevity.

public class RetryStrategy : IRetryStrategy
{
    public TResult ExecuteWithRetry<TResult>(IDbCommand commandContext, Func<TResult> func)
    {
        int attempt = 0;
        TimeSpan delay = MinBackOff;

        while (true)
        {
            try
            {
                return func();
                //     ^^^^^
            }
            catch (Exception ex)
            {
                // if it's not a transient error, then let it go
                if (!IsTransientException(ex))
                    throw;

                // if the number of retries has been exceeded then throw
                if (attempt >= MaxRetryCount)
                    throw;
                    
                // some lines removed for brevity

                // wait before retrying the command
                // unless this is the first attempt or first retry is disabled
                if (attempt > 0 || !FastFirstRetry)
                {
                    Thread.Sleep(delay);

                    // update the increment
                    delay += IncrementalBackOff;
                    if (delay > MaxBackOff)
                        delay = MaxBackOff;
                }

                // increment the attempt
                attempt++;
            }
        }
    }
}

And, this is how to use the method to open a connection.

public class ReliableConnection : DbConnectionWrapper
{
    public override void Open()
    {
        RetryStrategy.ExecuteWithRetry(null, () => { InnerConnection.Open(); return true; });
    }
}

Voilà! That’s the difference between Func and Action. Remember that they only represent the signature of a method. You can define or pass around the body later.

If you want to know more about LINQ, check my quick guide to LINQ. All you need to know about LINQ in 15 minutes or less. To learn how to use Insight.Database, check how to create a CRUD API with ASP.NET Core and Insight.Database.

Happy Funcy time!

Parsinator, a tale of a pdf parser

Imagine one day, your boss asks you to read a pdf file to extract relevant information and build a request for your main API. That happened to me. My first thought was: “how in the world am I going to read the text on the pdf file?” This is how I built Parsinator.

Parsinator is a library to turn structured and unstructured text into a header-detail representation. With Parsinator, we can create an XML file from a text-based pdf file or a C# object from a printer spool file.

I wrote Parsinator to parse invoices into XML files and then to call a documents API on an invoicing system.

Requirements

There I was, another day at the office with a new challenge. One of our clients couldn’t connect to our invoicing software. The only input he could provide was a text-based pdf file. This was the challenge: parse a text-based pdf file into an XML file.

These were the requirements:

  • I could receive pdf files with any structure. Two clients won’t have the same file structure.
  • I will receive not only pdf files but any plain-text file.
  • I needed to build something easy to grasp for my coworkers or future self to maintain.
  • I will have to ship it by the end of the week. Sounds familiar?

Actual implementation

I couldn’t use regular expressions on every line of the input text. That wasn’t a good solution. Every new file would imply coding the whole thing again. Arrrggg!

One of my concerns was extracting the text from a pdf file. But, after Googling a bit, I found the iTextSharp library and a StackOverflow answer to read a text-based pdf file. No big deal after all!

After using iTextSharp, a pdf file was a list of lists of strings, List<List<string>>. One list per page and one string per line. I abstracted this step to support any text, not only pdf files.

My next concern was how to do the actual parsing. I borrowed Parser combinators from Haskell and other functional languages. I could create small composable functions to extract or discard text at the page or line level.

Skippers

First, I assumed that a file has some content that spawns from one page to another. Imagine an invoice with many purchased items that we need a couple of pages to print it. Also, I assumed that a file has some content on a particular page and line. For example, the invoice number is at the top-right corner of the first page.

Second, there were some lines I could ignore. For example, the legal notice at the bottom of the last page of an invoice. I needed to “skip” the first or last lines on a page, all blank lines, and everything between two line numbers or regular expressions. To ignore some text, I wrote skippers.

This is SkipLineCountFromStart, a skipper to ignore the first lines of every page:

public class SkipLineCountFromStart : ISkip
{
    private readonly int LineCount;

    public SkipLineCountFromStart(int lineCount = 1)
    {
        this.LineCount = lineCount;
    }
            
    // The first LineCount lines are removed from the 
    // input text
    public List<List<string>> Skip(List<List<string>> lines)
    {
        var skipped = lines.Select(l => l.Skip(LineCount).ToList())
                           .ToList();
        return skipped;
    }
}
Parsinator, a tale of a pdf parser
Photo by Carli Jeen on Unsplash

Parsers

After ignoring unnecessary text, I needed some functions to extract the text between two lines or regular expressions. I called these functions: parsers.

This is ParseFromLineNumberWithRegex, a parser to read a line at a given number if it matches a regular expression:

public class ParseFromLineNumberWithRegex : IParse
{
    private readonly string Key;
    private readonly int LineNumber;
    private readonly Regex Pattern;
        
    public ParseFromLineNumberWithRegex(string key, int lineNumber, Regex pattern)
    {
        this.Key = key;
        this.LineNumber = lineNumber;
        this.Pattern = pattern;
    }
        
    // Parse if the given line matches a regex and
    // returns the first matching group
    public IDictionary<string, string> Parse(string line, int lineNumber)
    {
        if (lineNumber == this.LineNumber)
        {
            var matches = this.Pattern.Match(line);
            if (matches.Success)
            {
                HasMatched = true;
                var value = matches.Groups[1].Value;
                return new Dictionary<string, string> { { Key, value.Trim() } };
            }
        }
        return new Dictionary<string, string>();
    }
}

Transformations

But what about the text spawning many pages? I came up with transformations. Well I almost named them “Transformers,” but I didn’t want to confuse them with the giant robots from the movies… Transformations flatten all lines spawning many pages into a single stream of lines.

Imagine an invoice with a table of all purchased items spawning into two or more pages. The items table starts with a header and ends with a subtotal. I could use some skippers to extract these items. Then, I could apply parsers in every line to find the item name, quantity, and price.

This is TransformFromMultipleSkips, a transformation that applies some skippers to generate a single stream of text:

public class TransformFromMultipleSkips : ITransform
{
    private readonly IList<ISkip> ToSkip;

    public TransformFromMultipleSkips(IList<ISkip> skippers)
    {
        ToSkip = skippers;
    }

    public List<string> Transform(List<List<string>> allPages)
    {
       // Chain applies the next skipper on the output of the previous one
        List<String> details = ToSkip.Chain(allPages)
                                     .SelectMany(t => t)
                                     .ToList();
        return details;
    }
}

This is how I could use the previous transformation to grab the purchased items of an invoice:

// Table starts with "Code Description Price Total"
// and ends with "S U B T O T A L"
new TransformFromMultipleSkips(
    new SkipBeforeRegexAndAfterRegex(
        before: new Regex(@"\|\s+Code\s+.+Total\s+\|"),
        after: new Regex(@"\|\s+\|\s+S U B T O T A L\s+\|")),
    new SkipBlankLines());

I used two skippers: one to ignore everything before and after two regular expressions and another to ignore blank lines.

All the pieces

Then, I created a method to put everything in place. It applied all skippers on every page to ignore the irrelevant text. After that, it runs all parsers in the appropriate pages and lines from the output of skippers.

This is the Parse method:

public Dictionary<string, Dictionary<string, string>> Parse(List<List<string>> lines)
{
    List<List<string>> pages = _headerSkipers.Chain(lines);

    foreach (var page in pages.Select((Content, Number) => new { Number, Content }))
    {
        var parsers = FindPasersForPage(_headerParsers, page.Number, lines.Count);
        if (parsers.Any())
            ParseOnceInPage(parsers, page.Content);
    }

    if (_detailParsers != null && _detailParsers.Any())
    {
        List<String> details = (_transform != null)
                ? _transform.Transform(pages)
                : pages.SelectMany(t => t).ToList();

        ParseInEveryLine(_detailParsers, details);
    }

    return _output;
}

Conclusion

Voilà! That’s how I came up with Parsinator. With this approach, I could parse new files without coding the whole thing every time I needed to support a new file structure. I only needed to reuse the right skippers and parsers.

I used Parsinator to connect 4 clients with legacy software to an invoicing software by parsing pdf and plain text files to input XML files.

In the Sample project, I wrote tests to parse a plain-text invoice and a GPS frame. Feel free to take a look at it.

All ideas and contributions are more than welcome!

canro91/parsinator - GitHub

The C# Definitive Guide

Are you looking for a learning path to be “fluent” in C#? This is the right place for you! This is my definitive guide to what every beginner and intermediate C# developer should know.

Every intermediate C# developer should know how to productively work with Visual Studio or Visual Studio Code, use async/await keywords, most common LINQ methods and regular expressions. Also, to get around large codebases and be aware of the latest C# features.

1. Environment

Visual Studio is the de-facto Integrated Development Environment (IDE) for C#. Since we will spend most of our workdays with Visual Studio, we should setup Visual Studio to make use more productive.

  • Find a colorscheme. For example, I prefer the Solarized theme
  • Learn the basic shortcuts:
    • Ctrl + Shift + b: Build our solution
    • Ctrl + ,: Navigate to any method in our solution
    • Ctrl + .: Apply a refactor or any action in the current code block
    • Ctrl + q: Search and execute settings or menus of Visual Studio
    • Ctrl + Shift + F12: Go to the next error
  • Install some plugins to make our life easier. Like,
  • Use C# Interactive. We don’t have to create a dummy Console project to try things out. With C# interactive, we have a C# REPL at our disposition. We can load a NuGet package, a dll, or a C# script (.csx file). From Visual Studio, head to View Menu, Other Windows and click C# Interactive.

For more settings and extensions, check my Visual Studio setup for C#.

The C# Definitive Guide
Have everything ready to level up your C#. Photo by Andrew Neel on Unsplash

2. Git and Github

Git

Git is a version control system. A time machine to go back in time, create alternate stories from a point in time and make alternate stories join our present. You got the analogy?

If we are creating a zip file with our code and naming it after the date of our latest change, Git is a better way.

GitHub

Programming is about collaboration. GitHub is the social network for programmers.

With GitHub, we can show our own code, ask for new features in a library, and report bugs in the software we use.

Microsoft, Facebook, Google have some of their own code available on GitHub.

3. Design Patterns and Object-Oriented Design Principles

Desing patterns are recipes to solve common problems in code. This is, given a certain problem, there is a blueprint or an outline that will help us to solve that problem.

  • Recognize some of the most common patterns and learn to use them. For example: Factory, Builder, Composite, Command, Template, Strategy, Null Object, Adapter.
  • Check my take on the Decorator and Pipeline patterns.
  • Learn Uncle Bob’s SOLID principles.

4. Dealing with large codebases

Programming is also about reading code. Get used to navigate throught large codebases.

  • Find a library or a tool you have already used or you find useful. For example, DateTimeExtensions, ByteSize, Insight.Database
  • Where are the unit tests? Do they follow a folder structure? Identify a naming convention for them
  • Does the project follow certain code convention? For example, are braces written on the same line?
  • Grab a copy and compile it yourself
  • Debug a test case scenario for a feature you would like to know about
  • Find out how a feature was implemented

For more guidelines about reading code, check Changelog’s One sure-fire way to improve your coding.

5. Unit tests

A unit test is a “safety net” to make sure we don’t break things when we add new features or modify our codebase. A unit test is a piece of code that uses our code base from a “user” point of view and verifies a given behavior.

6. LINQ

Language-Integrated Query, LINQ, is the declarative way to work with collections in C# or anything that looks like one. Instead of writing foreach, for or while loops to work with collections, let’s give LINQ a try.

7. Regular Expressions

Have you ever used *.txt in the file explorer to find all text files in a folder? If so, we have already used regular expressions. But, *.txt is just the tip of the iceberg.

Regular expressions give us a search syntax to find patterns of text in a string. For example, to find all phone numbers like this one (+57) 3XX XXX-XXX, let’s use (\(\+\d{2}\))\s(\d{3})\s(\d{3})\-(\d{3}).

  • Learn the basics
    • Character sets: [] and [^]
    • Shorthand: \d for digits, \w for alphanumeric chars, \s for whitespace.
    • Repetions: *, ?, {min,max}
    • Any character: the dot .
    • Escape reserved characters: ^$()[]\|-.*+
    • Groups
  • Learn how to match and replace a regex in C#. Take a look at Match, IsMatch, Replace methods in Regex class.
  • Learn how to acess named groups in C#
  • Read Regular expressions’ Quickstart

8. async/await

Asyncronous code is code that doesn’t block when executing long-running operations.

9. New C# features

C# is an evolving language. With every new version, we have more features to write more concise code. These are some of the new features in C# since C# 6.0.

String interpolation

Before we wrote,

string.Format("Hello, {0}", name);

Now we can write,

$"Hello, {name}";

Null-conditional operators

There are two new operators to check for null values: ?? and ?..

Before,

string name = ReadNameFromSomewhere();
if (name == null)
    name = "none";
else
    name.Trim();

After,

string name = ReadNameFromSomewhere();
name?.Trim() ?? "none"

Inlined out variables

Now, we can inline the variable declaration next to the out keyword.

Before,

int count = 0;
int.TryParse(readFromKey, out count);

After,

int.TryParse(readFromKey, out var count)

Or even,

int.TryParse(readFromKey, out _)

Using declarations

A variable preceded by using is disposed at the end of the scope.

Before,

using (var reader = new StreamReader(fileName))
{
    string line; 
    while ((line = reader.ReadLine()) != null)  
    {  
        // Do something  
    }  
}

After,

using var reader = new StreamReader(fileName);

string line; 
while ((line = reader.ReadLine()) != null)  
{  
    // Do something  
}

Nullable reference types

All reference variables are non-nullable by default. Any attempt to dereference a nullable reference gets a warning from the compiler. Goodbye, NullReferenceException!

We need to turn on this feature at the project level in our csproj files.

Before,

int notNull = null;
//  ^^^^^
// error CS0037: Cannot convert null to 'int'
int? canBeNull = null;

string name = null;
SayHi(name);
// ^^^^^
// System.NullReferenceException

void SayHi(string name) => Console.WriteLine(name.Trim()); 

After,

string name = null;
// ^^^^^
// warning CS8600: Converting null literal or possible null value to non-nullable type.

string? canBeNullName = null;
SayHi(name);
// ^^^^^
// warning CS8604: Possible null reference argument for parameter 'name'

To learn other techniques to prevent the NullReferenceException, start checking what the NullReferenceException is and when it’s thrown.

Records

A record is an immutable reference type with built-in equality methods. When we create a record, the compiler creates a ToString method, a value-based equality methods and other methods for us.

public record Movie(string Title, string ReleaseYear);

Top-level statements

All the boilerplate is now gone from Main methods.

Before,

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

After,

Console.WriteLine("Hello World!");

To learn about other C# features, check my top 10 or so best C# features.

10. Bonus Points

Voilà! That’s my take on what every intermediate C# developer should know! Don’t be overwhelm by the amount of things to learn. Don’t try to learn everything at once, either. Learn one subject at a time! And, start using it in your every day coding as you learn it.

Want to write more expressive code for collections? Join my course, Getting Started with LINQ on Udemy and learn everything you need to know to start working productively with LINQ—in less than 2 hours.

Happy coding!