Introduction to C# Language Features
C# is a powerful, modern, and object-oriented programming language developed by Microsoft. It continuously evolves, introducing features that enhance developer productivity, code safety, and performance. This documentation explores key language features that form the backbone of modern .NET development.
Understanding these features is crucial for writing efficient, maintainable, and robust applications. Let's dive into some of the most impactful ones.
Generics
Generics allow you to define type-safe collections and methods without specifying the exact data type. This improves code reusability and performance by eliminating the need for casting and boxing.
What are Generics?
Generics introduce parameters to types. You can declare a class, interface, or method to operate on a type that is specified as a parameter. For example, a generic List<T>
can hold elements of any type T
, ensuring type safety at compile time.
Example: Generic List
using System.Collections.Generic;
// A list of integers
List<int> numbers = new List<int>();
numbers.Add(10);
// numbers.Add("hello"); // Compile-time error!
// A list of strings
List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
LINQ (Language Integrated Query)
LINQ provides a powerful and consistent way to query data from various sources, such as collections, databases, and XML documents, directly within the C# language.
Key Concepts
LINQ unifies query capabilities across different data sources. It uses a declarative syntax that makes queries readable and expressive.
- Query Syntax: Similar to SQL syntax (e.g.,
from x in collection where ... select ...
). - Method Syntax: Uses extension methods (e.g.,
collection.Where(...).Select(...)
).
Example: Querying a List of Numbers
using System.Linq;
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
// Query syntax to find numbers greater than 3
var querySyntax = from num in numbers
where num > 3
select num;
// Method syntax to find numbers less than 5
var methodSyntax = numbers.Where(num => num < 5);
foreach (var n in querySyntax)
{
Console.WriteLine(n); // Output: 5, 4, 9, 8, 6, 7
}
Asynchronous Programming (async/await)
The async
and await
keywords simplify the process of writing asynchronous code, making applications more responsive, especially in UI or I/O-bound scenarios.
How it Works
async
marks a method as asynchronous, allowing it to use the await
keyword. await
suspends the execution of the async
method until the awaited task completes, without blocking the thread. This is crucial for maintaining UI responsiveness and improving server scalability.
Example: Asynchronous File Read
using System.IO;
using System.Threading.Tasks;
public class AsyncExample
{
public async Task<string> ReadFileContentAsync(string filePath)
{
string content = await File.ReadAllTextAsync(filePath);
return content;
}
public async Task ProcessFile(string path)
{
Console.WriteLine("Starting file read...");
string data = await ReadFileContentAsync(path);
Console.WriteLine("File read complete. Content length: " + data.Length);
}
}
Pattern Matching
Pattern matching provides a more concise and powerful way to check the type of an object and extract data from it, enhancing switch statements and conditional logic.
Advanced Patterns
C# supports various patterns, including type patterns, property patterns, list patterns, and relational patterns, making code more readable and less error-prone.
Example: Property Pattern Matching
public record Point(int X, int Y);
public string DescribePoint(object obj) => obj switch
{
Point { X: 0, Y: 0 } => "Origin",
Point { X: var x, Y: var y } when x == y => $"On the line y=x with X={x}",
Point { X: var x } => $"Has X coordinate {x}",
_ => "Not a point"
};
// Usage:
// Console.WriteLine(DescribePoint(new Point(0, 0))); // Output: Origin
// Console.WriteLine(DescribePoint(new Point(5, 5))); // Output: On the line y=x with X=5
// Console.WriteLine(DescribePoint(new Point(10, 2))); // Output: Has X coordinate 10
// Console.WriteLine(DescribePoint(new { Name = "Test" })); // Output: Not a point
Nullable Reference Types
This feature helps developers avoid common NullReferenceException
errors by providing compile-time checks for potential null values in reference types.
Enabling and Usage
When enabled (typically in the project file or Properties/Settings.settings
), reference types are non-nullable by default. You can explicitly mark a type as nullable using a ?
suffix (e.g., string?
).
Example: Nullability Checks
// Assuming Nullable Reference Types are enabled
string nonNullableName = "Alice";
// nonNullableName = null; // Warning: Possible null assignment
string? nullableName = "Bob";
nullableName = null; // Allowed
// Safer access to nullable types
if (nullableName is not null)
{
Console.WriteLine(nullableName.ToUpper());
}
Records
Records, introduced in C# 9, are a special kind of class or struct designed for data storage. They offer concise syntax for immutable data types with built-in value equality.
Benefits
Records automatically generate methods like Equals()
, GetHashCode()
, ToString()
, and support value-based equality and structural sharing, making them ideal for representing immutable data.
Example: Record Declaration
// Positional record syntax
public record Person(string FirstName, string LastName, int Age);
// Usage:
var person1 = new Person("John", "Doe", 30);
var person2 = new Person("John", "Doe", 30);
var person3 = person1 with { Age = 31 }; // Creates a new record with updated age
Console.WriteLine(person1); // Output: Person { FirstName = John, LastName = Doe, Age = 30 }
Console.WriteLine(person1 == person2); // Output: True (value equality)
Console.WriteLine(person1 == person3); // Output: False
Discards
Discards allow you to ignore a return value or an out parameter by using the underscore (_
) character as a variable name.
When to Use
This is useful when you don't need to use certain values returned from a method or assigned to a variable. It makes the code clearer by explicitly indicating that a value is not being used.
Example: Ignoring Out Parameters
// Example method with an out parameter
bool TryParseInt(string s, out int result)
{
return int.TryParse(s, out result);
}
// Using discards to ignore the result of TryParseInt
if (TryParseInt("123", out _))
{
Console.WriteLine("Successfully parsed an integer.");
}
// Another example with Tuple deconstruction
var dictionary = new Dictionary<string, int> { {"one", 1}, {"two", 2} };
if (dictionary.TryGetValue("two", out _))
{
Console.WriteLine("Key 'two' exists.");
}