Introduction
As a .NET developer, I have noticed that there are very important concepts that help us up-level our coding skills. This article is the first in the Dotnet-Foundation series that I will be sharing with you. In this series, I will focus on introducing various concepts — some will be theoretical, some will include illustrative code, or references to original documentation to help you easily grasp these critically important concepts. In the future, I plan to move toward a System-Design In Practice series, and it will definitely be based on real-world scenarios where we will apply these theories and concepts extensively. But first, let’s explore the concepts that every C#/.NET developer should know!
1. Primitive Data Types in C#
Primitive data types are built-in types in the C# language, used to store simple values such as integers (int), floating-point numbers (float, double), characters (char), and boolean values (bool). These form the foundation for building more complex structures.
- Integers (int, long, short): Stored on the stack, providing fast access and no memory fragmentation.
- Floating-point numbers (float, double, decimal): Commonly used for calculations involving real numbers, but decimal is preferred for financial processing due to its higher precision.
- Boolean (bool): Stores true or false values, commonly used in conditional structures.
- Char: Represents a single Unicode character, stored as 16-bit.
using System;
class CircleAreaCalculator
{
static void Main()
{
const double Pi = 3.14159; // Hằng số Pi
double radius;
Console.WriteLine("Nhập bán kính hình tròn:");
radius = Convert.ToDouble(Console.ReadLine());
double area = Pi * radius * radius;
Console.WriteLine($"Diện tích hình tròn là: {area}");
}
}
Notes:
- The stack operates more efficiently than the heap, so value types are generally processed faster than reference types.
- Choosing the correct data type will make your code more solid and can also have a positive impact on performance.
2. Data Types and Variables in C#
Variables in C# are temporary storage locations in memory. Understanding how to declare and use data types properly helps optimize performance and reduce errors.
- There are two main types of variables:
- Value Type: Stores the value directly.
- Reference Type: Stores a reference address pointing to the value on the heap.
- Variables can be declared with the var keyword for automatic type inference.
using System;
class TypeConversionExample
{
static void Main()
{
string numberStr = "42";
int numberInt = int.Parse(numberStr); // Ép kiểu tường minh
double numberDouble = numberInt; // Ép kiểu ngầm định
Console.WriteLine($"Số nguyên: {numberInt}");
Console.WriteLine($"Số thực: {numberDouble}");
}
}
Notes:
- Implicit casting does not incur additional processing overhead.
- Explicit casting can cause runtime errors if not checked beforehand.
- Using var may make code harder to read, but does not affect performance.
Best practices:
- Declare variables with the narrowest scope possible to reduce memory overhead.
- Avoid using var when the data type is not obvious, to improve code readability.
- Check data types before casting, especially with incompatible types.
- Use local variables instead of global variables to reduce resource consumption.
3. Operators and Expressions in C#
Operators are symbols used to perform operations on variables and values.
- Arithmetic operators: +, -, *, /, % are used for basic mathematical operations.
- Comparison operators: ==, !=, <, > check the relationship between two values.
- Logical operators: &&, ||, ! are used to combine conditions.
- Ternary operator: (condition) ? trueResult : falseResult is a shorthand for if-else structures.
using System;
class OddEvenChecker
{
static void Main()
{
Console.WriteLine("Nhập một số:");
int number = int.Parse(Console.ReadLine());
string result = (number % 2 == 0) ? "chẵn" : "lẻ";
Console.WriteLine($"Số {number} là số {result}.");
}
}
Notes:
- Using arithmetic operators like % can be slower than bitwise checks in certain specific cases.
- Short-circuit logical operators like &&, || improve performance by stopping evaluation when unnecessary.
Best practices:
- Prefer short-circuit operators (&&, ||) to reduce unnecessary evaluations.
- Avoid complex expressions in conditions to improve clarity and performance.
- Use the checked operator to detect overflow in arithmetic operations.
4. Control Structures (if-else, loops)
Control structures allow code execution based on conditions or repeated execution of code blocks.
- Conditional structures (if-else): Direct program flow.
- Loops (for, while, do-while): Repeat code blocks based on conditions.
- foreach loop: Iterate through elements in a collection.
using System;
class OddSumCalculator
{
static void Main()
{
int sum = 0;
for (int i = 1; i <= 10; i++)
{
if (i % 2 != 0)
sum += i;
}
Console.WriteLine($"Tổng các số lẻ từ 1 đến 10 là: {sum}");
}
}
Notes:
- The for loop is faster than foreach when working with arrays, as it avoids the iterator overhead.
- break and continue help reduce unnecessary iterations, improving performance.
Best practices:
- Use break to exit loops when a condition is met.
- Avoid deeply nested loops; use more optimal algorithms instead.
- When iterating over large collections, consider using Parallel.For to improve performance.
5. Methods and Functions
Methods are blocks of code that perform a specific task and can be called from multiple places in the program, enabling code reuse and better organization.
- Methods with return values: Use the return keyword to return a value.
- Methods without return values: Use the void keyword.
- Parameters:
- Required parameters: Must provide a value when calling the function.
- Optional parameters: Have default values.
- Recursion: A method that calls itself.
using System;
class FactorialCalculator
{
static int Factorial(int n)
{
if (n == 0 || n == 1) return 1;
return n * Factorial(n - 1);
}
static void Main()
{
Console.Write("Nhập số cần tính giai thừa: ");
int num = int.Parse(Console.ReadLine());
Console.WriteLine($"Giai thừa của {num} là: {Factorial(num)}");
}
}
Notes:
- Recursion uses the stack to store function state, which can lead to stack overflow errors if not properly controlled.
- Methods with optional parameters reduce the number of overloads needed, making code more concise.
Best practices:
- Avoid using recursion if it can be replaced with a loop to prevent stack overflow errors.
- Use async and await when handling asynchronous methods.
- Name methods clearly, accurately describing their task.
6. Classes and Objects
A class is a blueprint for objects, while an object is an instance of a class. This is the foundation of object-oriented programming.
- A class consists of:
- Properties: Used to store data.
- Methods: Used to manipulate data.
- Objects: Created from classes using the new keyword.
- Encapsulation: Restricts direct access to data through private and public access modifiers.
using System;
class Car
{
public string Model { get; set; }
public string Color { get; set; }
public void Start()
{
Console.WriteLine($"{Color} {Model} đang khởi động.");
}
}
class Program
{
static void Main()
{
Car myCar = new Car { Model = "Toyota", Color = "Xanh" };
myCar.Start();
}
}
Notes:
- Objects created on the heap need to be garbage collected, leading to higher memory management costs.
- Encapsulation improves security and reduces errors, but may increase data access overhead.
Best practices:
- Use encapsulation (private, public, protected) to protect data.
- Use the readonly keyword for read-only properties.
- Avoid creating unnecessary objects to reduce pressure on the garbage collector.
7. Inheritance and Polymorphism
Inheritance allows a child class to inherit properties and methods from a parent class. Polymorphism allows an object to take on many different forms.
- Inheritance: Child classes use the : symbol to extend parent classes.
- Polymorphism:
- Virtual methods: Can be overridden by child classes.
- Abstract methods: Must be implemented by child classes.
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Động vật kêu.");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Chó sủa: Gâu gâu!");
}
}
class Program
{
static void Main()
{
Animal myDog = new Dog();
myDog.Speak(); // Gọi phương thức ghi đè của lớp Dog
}
}
Notes:
- Virtual and override methods require an additional lookup table (vtable), making them slower than non-virtual methods.
- Improper use of inheritance can increase complexity.
Best practices:
- Use sealed to prevent child classes from inheriting a class when unnecessary.
- Use polymorphism when flexible extensibility is needed; avoid overriding when not necessary.
- Do not overuse inheritance; consider using composition instead.
8. Interfaces and Abstract Classes
Interfaces define behaviors that a class must implement. Abstract classes provide a common framework and can contain default logic.
- Interface:
- Only defines methods, no logic.
- A class can implement multiple interfaces.
- Abstract class:
- Can contain abstract methods or pre-built logic.
- A child class can only inherit from one abstract class.
using System;
interface ILogger
{
void Log(string message);
}
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
class Program
{
static void Main()
{
ILogger logger = new ConsoleLogger();
logger.Log("Hello, World!");
}
}
Notes:
- Calling methods through an interface has higher overhead compared to direct calls.
- Abstract classes can use default logic, reducing code duplication.
Best practices:
- Use interfaces to increase flexibility and extensibility.
- Use abstract classes when shared logic is needed for child classes.
- Avoid overusing interfaces or creating too many unnecessary abstract classes.
9. Exception Handling
Exception handling in C# helps programs handle errors safely and avoid unexpected termination.
- An Exception is an error that occurs while the program is running, such as division by zero or accessing an array out of bounds.
- The try-catch-finally block:
- try: Contains code that may generate errors.
- catch: Handles errors if they occur.
- finally: Executes code regardless of whether an error occurred.
- Creating custom exceptions: Inherit from the Exception class.
using System;
class ExceptionHandlingExample
{
static void Main()
{
try
{
Console.Write("Nhập số bị chia: ");
int dividend = int.Parse(Console.ReadLine());
Console.Write("Nhập số chia: ");
int divisor = int.Parse(Console.ReadLine());
int result = dividend / divisor;
Console.WriteLine($"Kết quả: {result}");
}
catch (DivideByZeroException)
{
Console.WriteLine("Lỗi: Không thể chia cho 0.");
}
catch (FormatException)
{
Console.WriteLine("Lỗi: Vui lòng nhập số hợp lệ.");
}
finally
{
Console.WriteLine("Cảm ơn bạn đã sử dụng chương trình.");
}
}
}
Notes:
- Throwing and catching exceptions is CPU-expensive. Avoid using exceptions for regular condition checking.
- Using many catch blocks increases complexity and can affect performance.
Best practices:
- Only use exceptions for unavoidable errors.
- Always use finally to release resources.
- Log detailed error information for easier troubleshooting.
10. Arrays and Lists
Arrays and Lists are fundamental data structures for storing collections of elements in C#.
- Array:
- Fixed size, stores elements of the same data type.
- Fast access by index.
- List
: - Dynamic size, flexible for adding/removing elements.
- Supports many LINQ methods.
using System;
using System.Collections.Generic;
class ArrayAndListExample
{
static void Main()
{
// Mảng
int[] array = { 1, 2, 3, 4, 5 };
int arraySum = 0;
foreach (var item in array)
{
arraySum += item;
}
Console.WriteLine($"Tổng mảng: {arraySum}");
// Danh sách
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
list.Add(6); // Thêm phần tử
int listSum = 0;
foreach (var item in list)
{
listSum += item;
}
Console.WriteLine($"Tổng danh sách: {listSum}");
}
}
Notes:
- Array: Faster access than lists since it does not need to handle dynamic structure overhead.
- List: Consumes more memory when resizing during element additions. Best practices:
- Use arrays when the data size is known in advance.
- Use List
when flexible adding/removing of elements is needed. - Avoid resizing arrays or lists multiple times to reduce memory overhead.
Conclusion
In this article, I have introduced 10 concepts that I consider fundamental and that every developer should understand. Of course, the scope of this article is introductory, and the theory may be a bit dry. But trust me, mastering any discipline requires a solid grasp of theory combined with practice — practicing over and over again is the only way to truly succeed. In the next article, I will continue to introduce 10 more concepts, and I think they will be a bit more advanced. Stay tuned. Thanks for reading!
