Q1. What is a Class and an Object? How are they different?
A class is a blueprint or template that defines the structure and behavior of objects. It is a logical entity that exists only in code and is used to create instances. An object, on the other hand, is a concrete instance of a class that exists in memory and contains actual data. A class is an abstract concept, while an object is a tangible entity created from a class using the new keyword. For example, "Car" could be a class, while "My Toyota Camry" would be a specific object of the Car class. Multiple objects can be created from a single class, and each object maintains its own separate state and data.
Example:
// Class definition (Blueprint)
public class Car
{
public string Model { get; set; }
public string Color { get; set; }
public void Drive()
{
Console.WriteLine($"Driving {Model} car");
}
}
// Objects (Instances of the class)
Car car1 = new Car { Model = "Honda", Color = "Red" };
Car car2 = new Car { Model = "Toyota", Color = "Blue" };
car1.Drive(); // Output: Driving Honda car
car2.Drive(); // Output: Driving Toyota car
Q2. What is Encapsulation? Why is it important in OOPs?
Encapsulation is one of the four pillars of OOPs that bundles data (variables) and methods (functions) that operate on that data into a single unit called a class. It also hides internal implementation details from the outside world by restricting direct access to the object's data. The primary benefit of encapsulation is that it allows you to control how data is accessed and modified through public methods while protecting the internal state of objects. This promotes data integrity, reduces complexity, and makes code more maintainable. It also provides flexibility to change internal implementations without affecting external code that uses the class.
Q3. What is Inheritance? Explain different types of Inheritance.
Inheritance is a mechanism where a new class (derived class) inherits properties and methods from an existing class (base class). It promotes code reusability and establishes a hierarchical relationship between classes. C# supports several types of inheritance: (1) Single Inheritance - a class inherits from one base class; (2) Multilevel Inheritance - a derived class becomes the base class for another class; (3) Hierarchical Inheritance - multiple classes inherit from a single base class; (4) Multiple Inheritance - achieved through interfaces in C# since C# doesn't support multiple class inheritance. Inheritance enables polymorphism and allows you to create specialized classes that extend the functionality of existing classes without modifying them.
Q4. What is Polymorphism? Explain Compile-time and Runtime Polymorphism.
Polymorphism means "many forms" and is the ability of objects to take multiple forms. It allows you to write flexible and reusable code by enabling objects to behave differently based on context. There are two types: (1) Compile-time Polymorphism (Static Polymorphism) - achieved through method overloading and operator overloading. The compiler determines which method to call at compile time based on the method signature; (2) Runtime Polymorphism (Dynamic Polymorphism) - achieved through method overriding using virtual and override keywords. The actual method to call is determined at runtime based on the object type. Runtime polymorphism is more powerful and is typically used with inheritance and interfaces.
Example:
// Compile-time Polymorphism (Method Overloading)
public class Calculator
{
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
}
// Runtime Polymorphism (Method Overriding)
public class Animal { public virtual void Speak() { } }
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
Animal dog = new Dog();
Animal cat = new Cat();
dog.Speak(); // Output: Woof!
cat.Speak(); // Output: Meow!
Q5. What is Abstraction? How is it different from Encapsulation?
Abstraction is the process of hiding complex implementation details and showing only the essential features of an object. It focuses on "what" an object does rather than "how" it does it. In C#, abstraction is achieved through abstract classes and interfaces. Encapsulation, while related, is the bundling of data and methods together with access control, focusing on hiding the internal state and protecting data integrity. The key difference is that abstraction hides complexity (focusing on behavior), while encapsulation hides internal implementation details (focusing on data protection). Abstraction deals with classes and interfaces, whereas encapsulation deals with access modifiers like private, protected, and public. Both work together to create robust object-oriented designs.
Q6. What is an Interface? How is it different from an Abstract Class?
An interface is a contract that defines a set of methods and properties that a class must implement. It contains no implementation details and cannot be instantiated directly. An abstract class, conversely, is a class that cannot be instantiated and can contain both abstract methods (without implementation) and concrete methods (with implementation). Key differences include: (1) A class can implement multiple interfaces but inherit from only one abstract class; (2) Interfaces have no access modifiers (public by default), while abstract classes can have access modifiers; (3) Interfaces cannot have state (fields), while abstract classes can; (4) Interfaces are used for defining contracts for unrelated classes, while abstract classes define a common base for related classes; (5) Abstract classes promote code reuse through inheritance, while interfaces promote loose coupling through contracts.
Q7. What is a Sealed Class? When would you use it?
A sealed class is a class that cannot be inherited from any other class. Once you mark a class as sealed using the "sealed" keyword, no other class can derive from it. The primary purpose of sealed classes is to prevent inheritance when it's not intended or when allowing inheritance could lead to design issues. Sealed classes are useful when you want to prevent subclassing of a class for security reasons, performance optimization, or to maintain the integrity of a class design. Additionally, the compiler can perform optimizations on sealed classes since it knows the class won't be extended. You can also seal individual methods in a derived class to prevent further overriding in subclasses.
Example:
// Sealed class - cannot be inherited
public sealed class FinalClass
{
public void Display() => Console.WriteLine("This is a sealed class");
}
// This will cause a compilation error
// public class DerivedClass : FinalClass { } // ERROR!
// Can seal individual methods too
public class BaseClass
{
public virtual void Method1() { }
}
public class DerivedClass : BaseClass
{
public sealed override void Method1() { } // Cannot override in further derived classes
}
Q8. What is a Partial Class? What are its advantages?
A partial class is a class whose definition can be split into multiple files. Using the "partial" keyword, you can distribute a single class definition across several files, which helps organize code logically and keeps related functionality together. The main advantages of partial classes are: (1) they allow large classes to be organized into multiple manageable files, improving code organization; (2) they enable auto-generated code and hand-written code to coexist separately, which is useful in Visual Studio designers; (3) they facilitate team collaboration by allowing different developers to work on different parts of a class without conflicts; (4) they reduce file sizes and improve code readability. However, partial classes should be used judiciously as splitting a class across multiple files can reduce clarity if not organized properly.
Q9. What is a Static Class? Can it have instance members?
A static class is a class that cannot be instantiated and exists only for grouping static members (methods, properties, fields) together. It is declared using the "static" keyword and serves as a container for utility functions or methods that don't require state or object instances. Static classes cannot have instance members, constructors (other than a private parameterless constructor), or implement interfaces. All members of a static class must be static. The purpose of static classes is to provide utility functions and avoid unnecessary object creation, as there's no need to instantiate an object to use the members. Examples include System.Math, System.String manipulation methods, and utility classes for logging or configuration.
Q10. What is the difference between Static and Instance Members?
Static members belong to the class itself and are shared across all instances of that class. They exist in memory only once, regardless of how many objects are created. Static members are accessed using the class name (ClassName.Member), not through an object instance. Instance members, conversely, belong to individual objects and each instance maintains its own copy of the member. Instance members are accessed through objects (object.Member) and their values can differ between different instances. Static members are initialized when the class is first loaded, while instance members are initialized when a new object is created. Static members are useful for shared data and utility methods, while instance members represent object-specific state and behavior.
Q11. Explain Value Types and Reference Types in C#. What's the difference?
Value types store data directly in memory (on the stack), while reference types store a reference to data stored on the heap. Value types include int, double, bool, struct, and enum, while reference types include class, interface, delegate, and array. When you assign a value type variable to another, a copy of the value is created. When you assign a reference type variable to another, only the reference (memory address) is copied, not the actual data. This means multiple reference type variables can point to the same object in memory. Changes to a reference type object affect all variables referencing it, while changes to a value type only affect that variable. Value types are faster and more memory-efficient for small data, while reference types are better for complex objects and when multiple references are needed. Understanding this distinction is crucial for avoiding bugs and writing efficient code.
Q12. What is Boxing and Unboxing? Provide an example.
Boxing is the process of converting a value type to a reference type (object). The value type is wrapped in an object and placed on the heap. Unboxing is the reverse process - extracting the value from a boxed object and converting it back to a value type. Boxing occurs implicitly when a value type is assigned to an object variable or passed to a method expecting an object parameter. Unboxing requires explicit casting. Boxing and unboxing have performance overhead as they involve memory allocation and data copying, so they should be minimized in performance-critical code. Modern C# with generics has reduced the need for boxing/unboxing, as generic collections avoid this overhead by working with specific types.
Example:
int value = 42;
// Boxing - converting value type to reference type
object boxedValue = value; // value is wrapped in an object
// Unboxing - converting reference type back to value type
int unboxedValue = (int)boxedValue; // Explicit cast required
Console.WriteLine($"Original: {value}");
Console.WriteLine($"Boxed: {boxedValue}");
Console.WriteLine($"Unboxed: {unboxedValue}");
// Incorrect unboxing - throws InvalidCastException
// double wrongUnbox = (double)boxedValue; // ERROR!
Q13. What are Nullable Types? How are they useful?
Nullable types allow value types (which normally cannot be null) to have a null value, representing the absence of data. A nullable type is created using the "?" syntax (e.g., int?, double?, bool?). Nullable types are useful when you need to represent missing or undefined values, such as in database applications where a column might have a NULL value. The HasValue property indicates whether the nullable type contains a value, and the Value property retrieves the actual value. You can also use the null coalescing operator (??) to provide a default value if the nullable type is null. This is particularly valuable when working with databases, APIs, or business logic where optional fields are common. Without nullable types, you would need to use reference types or special sentinel values to represent null states.
Q14. What is the difference between readonly and const keywords?
Both readonly and const are used to define immutable values, but they have important differences. const values are determined at compile-time and their values are hardcoded into the code. readonly values are determined at runtime and can be initialized in the constructor. const variables must be initialized at the point of declaration, while readonly fields can be initialized in the constructor. const is implicitly static, so you access it through the class name, while readonly can be both static and instance. const values are replaced with their literal values at compile-time, improving performance, but readonly involves a slight runtime lookup. Use const for values that never change and are known at compile-time (like mathematical constants), and use readonly for values that may vary but should not be modified after initialization.
Q15. What are the different access modifiers in C#? Explain their scope.
C# provides five access modifiers to control the visibility and accessibility of class members: (1) public - accessible from anywhere, even outside the assembly; (2) private - accessible only within the same class (default for class members); (3) protected - accessible within the class and derived classes; (4) internal - accessible only within the same assembly; (5) protected internal - accessible within the same assembly and derived classes. Additionally, private protected (C# 7.2+) restricts access to derived classes within the same assembly. Choosing appropriate access modifiers is crucial for encapsulation and preventing unauthorized access to sensitive members. As a general practice, keep members private by default and expose only necessary members through public methods and properties.
Q16. What are Constructors? Explain different types of Constructors.
A constructor is a special method that is called automatically when an object is created from a class. It initializes the object's state and performs any setup required. There are several types of constructors: (1) Default Constructor - parameterless constructor created automatically by the compiler if no constructors are defined; (2) Parameterized Constructor - accepts parameters to initialize object with specific values; (3) Copy Constructor - initializes an object by copying values from another object of the same class; (4) Static Constructor - initialized the static members of a class and is called only once before any instance is created; (5) Private Constructor - prevents object instantiation from outside the class, useful for singleton patterns. Constructors do not have return types and must have the same name as the class. They enable controlled object initialization and allow you to enforce invariants and setup required state.
Q17. What is the Constructor Chaining? How is it implemented?
Constructor chaining is a technique where one constructor calls another constructor of the same class to reduce code duplication. It is implemented using the "this" keyword followed by the appropriate constructor signature. When you have multiple constructors with different parameters, you can have a parameterized constructor call another with more parameters, or a constructor call a base constructor using "base". This promotes code reusability and maintains consistency in initialization logic. Constructor chaining is particularly useful when you have complex initialization logic that needs to be shared across multiple constructors. It ensures that common setup code is executed only once and reduces maintenance issues.
Q18. What is a Destructor? Explain the difference between Destructor and Dispose method.
A destructor is a special method that is called automatically when an object is being destroyed or garbage collected. It has the same name as the class preceded by a tilde (~) and cannot have parameters or return types. Destructors are used to release unmanaged resources like file handles, database connections, or memory pointers. However, the timing of destructor execution is unpredictable as it depends on the garbage collector. The Dispose method, conversely, is a method that implements the IDisposable interface and is called explicitly to release resources immediately. Dispose provides deterministic resource cleanup, while destructors offer non-deterministic cleanup. Modern C# practice favors using Dispose with the "using" statement or IAsyncDisposable for proper resource management. Destructors are rarely used in modern C# because Dispose is more controllable and predictable.
Example:
public class ResourceManager : IDisposable
{
private bool disposed = false;
~ResourceManager() // Destructor
{
Dispose(false);
}
public void Dispose() // Dispose method
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Release managed resources
}
// Release unmanaged resources
disposed = true;
}
}
}
Q19. What is the IDisposable Pattern? Why is it important?
The IDisposable pattern is a design pattern for properly releasing unmanaged resources in .NET applications. It involves implementing the IDisposable interface and following a specific pattern to ensure resources are cleaned up correctly. The pattern includes implementing a Dispose method (public), a parameterized protected Dispose method (private), and a destructor for backup cleanup. The key benefit is deterministic resource management - resources are released immediately when Dispose is called rather than waiting for garbage collection. This is crucial for file handles, database connections, and other limited resources. The pattern also prevents accidental double-disposal and ensures that finalizers are suppressed to improve garbage collection performance. Using the "using" statement or declaration automatically calls Dispose when exiting the scope, making resource management safer and more predictable.
Q20. What is a Property? How are Auto-Implemented Properties useful?
A property is a member that provides a flexible mechanism to read, write, or compute the value of a private field. Properties use get and set accessors to control access to data and allow validation or calculations. Auto-implemented properties (auto-properties) are a shorthand syntax that automatically creates a private backing field, eliminating the need to explicitly write the backing field and the simple accessor implementation. They improve code readability and reduce boilerplate code significantly. Auto-properties can be initialized inline using property initializers. They are useful for simple getter-setter pairs but when you need custom logic, you can use full property syntax. Auto-properties promote encapsulation by hiding implementation details while providing a clean public interface.
Q21. What are Indexers? How do they work?
An indexer is a member that allows instances of a class or struct to be indexed like arrays. It enables you to use the square bracket notation ([]) to access elements of an object, making the syntax more intuitive and readable. Indexers are defined using the "this" keyword and can have multiple parameters. They can have both get and set accessors and support property validation. Indexers are particularly useful for collection-like classes where you want to provide array-like access to internal data. They improve the usability of custom collection classes and make code more intuitive. Indexers can be overloaded with different parameter types, allowing flexible ways to access data.
Q22. What is Operator Overloading? Provide examples.
Operator overloading allows you to redefine how operators work with custom types (classes and structs). By overloading operators, you can provide intuitive behavior for operations on objects of your class. Common overloadable operators include +, -, *, /, ==, !=, <, >, etc. Overloaded operators are defined as static methods using the "operator" keyword. Operator overloading improves code readability by allowing you to use familiar operator syntax instead of method calls. For example, a Complex number class can overload + to add complex numbers intuitively. However, operator overloading should be used judiciously and only when it creates intuitive and expected behavior. Over-using operator overloading can make code confusing and harder to understand.
Example:
public class Complex
{
public double Real { get; set; }
public double Imaginary { get; set; }
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
// Overload + operator
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
}
// Overload == operator
public static bool operator ==(Complex a, Complex b)
{
return a.Real == b.Real && a.Imaginary == b.Imaginary;
}
public static bool operator !=(Complex a, Complex b) => !(a == b);
}
Complex c1 = new Complex(3, 4);
Complex c2 = new Complex(1, 2);
Complex c3 = c1 + c2; // Uses overloaded + operator
Q23. What is a Delegate? How is it different from a Method?
A delegate is a type-safe function pointer or reference type that holds a reference to a method with a specific signature. Delegates enable you to pass methods as parameters, store method references, and call methods dynamically at runtime. While methods are concrete implementations, delegates are types that can refer to any method with a matching signature. Delegates are particularly useful for implementing callback mechanisms, event handling, and functional programming patterns. The main advantage of delegates is flexibility - you can assign different methods to the same delegate variable at runtime. Delegates support multicast delegates where multiple methods can be chained together and invoked sequentially. Common delegate usage includes defining custom event handlers and implementing callback functions for asynchronous operations.
Q24. What are Events? How do they relate to Delegates?
An event is a wrapper around a delegate that follows the publish-subscribe pattern. Events allow objects to send notifications to other objects without knowing the details about who is listening. Events are declared using the "event" keyword and based on a delegate type. Only the class that declares the event can raise it, while other classes can subscribe to or unsubscribe from the event using += and -= operators. This encapsulation prevents external code from assigning directly to the event or invoking it, ensuring controlled notification behavior. Events are fundamental to UI programming and the observer pattern. The publisher (the class raising the event) doesn't need to know about subscribers, promoting loose coupling and flexibility in code design.
Q25. What are Lambda Expressions? What are their benefits?
A lambda expression is an anonymous function that can be used to create delegates or expression trees. Lambda expressions are written using the "=>" operator and provide a concise syntax for writing small, inline functions. They can take parameters and return values just like regular methods. Lambda expressions are particularly useful with LINQ, delegates, and functional programming patterns. The syntax is shorter than traditional method declarations, improving code readability and reducing boilerplate. Lambda expressions can access variables from their enclosing scope (closures), making them flexible for various scenarios. They are heavily used in modern C# with LINQ queries, event handlers, and asynchronous programming. Lambda expressions can also be used to create expression trees for meta-programming and dynamic query construction.
Q26. What are Action, Func, and Predicate Delegates?
Action, Func, and Predicate are generic delegate types provided by the .NET framework that serve common purposes: (1) Action - a delegate that does not return a value (void return). Action<T> accepts parameters but returns void; (2) Func - a delegate that returns a value. Func<T, TResult> accepts parameters of type T and returns a value of type TResult; (3) Predicate - a delegate specifically designed for conditional logic. Predicate<T> accepts a parameter of type T and returns a boolean value. These built-in delegates eliminate the need to define custom delegate types for common scenarios. They work seamlessly with lambda expressions and LINQ, making code more concise and readable. Using these standard delegates promotes consistency and reduces code duplication across projects.
Q27. What are Generics? Why are they important?
Generics are a feature that allows you to write reusable code by creating classes, methods, and interfaces that work with different data types while maintaining type safety. Generics use type parameters (usually represented by T, K, V) that are specified when the generic type is used. The key benefits of generics are: (1) type safety - compile-time checking prevents type-related errors; (2) performance - no boxing/unboxing overhead for value types; (3) code reusability - write once, use with many types; (4) cleaner API - code is more readable and self-documenting. Generics are essential in .NET and are used extensively in collections, LINQ, and many framework classes. Without generics, you would need to cast objects or create separate classes for each data type, leading to code duplication and runtime type errors.
Q28. What are Generic Constraints? Provide examples.
Generic constraints restrict the types that can be used as type arguments in a generic class or method. They ensure that only types meeting specific criteria can be used, enabling safer and more powerful generic code. C# supports several constraint types: (1) where T : struct - T must be a value type; (2) where T : class - T must be a reference type; (3) where T : new() - T must have a parameterless constructor; (4) where T : BaseClass - T must inherit from BaseClass; (5) where T : IInterface - T must implement IInterface. Multiple constraints can be combined, and derived classes can have additional constraints. Constraints are checked at compile-time, ensuring type safety. They allow you to call methods or access properties on generic types that are guaranteed to exist.
Example:
public class Repository<T> where T : class, new()
{
public T Create()
{
return new T(); // Valid because T has new() constraint
}
}
public class NumberProcessor<T> where T : struct
{
public double Average(T[] numbers) where T : struct
{
return 0; // Ensures T is a value type
}
}
public class Comparer<T> where T : IComparable<T>
{
public T Max(T a, T b)
{
return a.CompareTo(b) > 0 ? a : b;
}
}
Q29. What are the commonly used Collection types in C#?
C# provides several collection types in System.Collections.Generic namespace, each optimized for different scenarios: (1) List<T> - ordered, indexable collection allowing duplicates; (2) Dictionary<TKey, TValue> - key-value pairs for fast lookups; (3) HashSet<T> - unordered collection of unique items; (4) Queue<T> - FIFO (first-in-first-out) collection; (5) Stack<T> - LIFO (last-in-first-out) collection; (6) LinkedList<T> - doubly-linked list for efficient insertion/deletion at any position; (7) SortedList<TKey, TValue> - sorted key-value pairs. Each collection has different performance characteristics for different operations. List is most commonly used for general-purpose collections, while Dictionary is preferred for lookups. Choosing the right collection type significantly impacts application performance.
Q30. What is LINQ? Explain different LINQ variants.
LINQ (Language Integrated Query) is a powerful feature that integrates query capabilities directly into C#, allowing you to query data from various sources using a consistent syntax. LINQ variants include: (1) LINQ to Objects - queries in-memory collections; (2) LINQ to SQL - queries SQL databases; (3) LINQ to Entity Framework - queries data through Entity Framework; (4) LINQ to XML - queries XML documents; (5) PLINQ (Parallel LINQ) - queries using multiple processors for parallel processing. LINQ expressions can be written in method syntax or query syntax, both compiling to the same intermediate language. LINQ provides methods like Where, Select, GroupBy, OrderBy, Join, etc., enabling expressive and readable data queries. LINQ eliminates the need to write loops and complex logic for data filtering and transformation, improving code clarity and reducing errors.
Q31. What is Exception Handling? Explain try, catch, finally blocks.
Exception handling is a mechanism to handle runtime errors gracefully, preventing application crashes. The try block contains code that might throw an exception. The catch block handles specific exceptions that occur in the try block, allowing you to respond appropriately. Multiple catch blocks can handle different exception types. The finally block executes regardless of whether an exception occurred, making it ideal for cleanup operations like closing files or releasing resources. The general flow is: try to execute code, if an exception occurs jump to the matching catch block, and after catch (if present) execute the finally block. Exception handling allows you to provide meaningful error messages, log errors, and maintain application stability. It's important to catch specific exceptions rather than the generic Exception class to provide appropriate handling for different error scenarios.
Q32. What is the throw statement? When should you use it?
The throw statement is used to manually throw an exception when an error condition is detected in your code. You throw an exception by creating an instance of an exception class and using the throw keyword. The throw statement stops execution and transfers control to the nearest catch block that can handle the exception type. You should throw exceptions when: (1) invalid parameters are passed to a method; (2) preconditions for method execution are not met; (3) an operation cannot complete successfully; (4) a critical error occurs that should be handled by the caller. Throwing exceptions is preferable to returning error codes as it forces the caller to handle the error and provides a clear execution flow. You can also re-throw an exception using just "throw" without arguments to preserve the original stack trace.
Q33. How do you create Custom Exceptions in C#?
Custom exceptions are created by inheriting from the Exception class (or a more specific exception class). Creating custom exceptions allows you to represent application-specific error conditions and provides clearer semantics to callers about what went wrong. Custom exception classes should follow naming conventions (ending with "Exception") and include constructors that accept error messages and inner exceptions. You can add custom properties and methods to provide additional context about the exception. Custom exceptions make error handling code more expressive and maintainable by allowing specific exception types to be caught independently. Well-designed custom exceptions improve code clarity and enable clients to distinguish between different error conditions in your application.
Example:
public class InvalidAgeException : Exception
{
public int ProvidedAge { get; set; }
public InvalidAgeException(string message, int age)
: base(message)
{
ProvidedAge = age;
}
public InvalidAgeException(string message, int age, Exception innerException)
: base(message, innerException)
{
ProvidedAge = age;
}
}
// Usage
public void SetAge(int age)
{
if (age < 0 || age > 150)
{
throw new InvalidAgeException("Age must be between 0 and 150", age);
}
}
Q34. What is the difference between throw and throw ex?
When re-throwing an exception in a catch block, "throw" (without arguments) preserves the original stack trace, while "throw ex" creates a new stack trace starting from the current throw statement. Using "throw" is the recommended approach because it provides more complete debugging information by maintaining the original stack trace showing where the exception originally occurred. Using "throw ex" loses the original stack trace and makes debugging more difficult as you won't see the full path of execution that led to the error. Always use "throw" when you want to re-throw an exception without modifications. This best practice helps developers quickly identify the root cause of errors during troubleshooting.
Q35. What are common Exception types in C#?
C# provides several built-in exception types for different error scenarios: (1) ArgumentException - invalid argument passed to a method; (2) ArgumentNullException - argument is null when non-null is required; (3) ArgumentOutOfRangeException - argument value is outside the allowed range; (4) IndexOutOfRangeException - array or collection index is invalid; (5) InvalidOperationException - operation is invalid for the current object state; (6) NotImplementedException - method is not implemented; (7) NullReferenceException - attempt to access member of a null object; (8) IOException - input/output operation fails; (9) FileNotFoundException - file cannot be found; (10) DivideByZeroException - division by zero occurs. Using the appropriate exception type helps callers understand what went wrong and handle it appropriately. You should throw specific exceptions rather than the generic Exception class.
Q36. What is Async/Await in C#? How does it work?
Async/Await is a feature that simplifies asynchronous programming by allowing you to write asynchronous code that looks like synchronous code. The async keyword marks a method as asynchronous, and await pauses execution at that point until the awaited task completes, then resumes execution with the result. Async methods return Task or Task<T> instead of void. When you await a task, the current thread is not blocked; instead, it can execute other work, improving application responsiveness and scalability. Async/Await makes it easier to work with I/O-bound operations like network requests, file operations, and database queries. It reduces callback complexity compared to traditional event-based asynchronous programming. Async methods should be used for I/O-bound operations, not CPU-bound operations.
Q37. What is the Task class? Explain Task vs Task<T>.
Task represents an asynchronous operation that may or may not return a value. Task<T> is a generic version that represents an asynchronous operation that returns a value of type T. Task is used when an asynchronous operation doesn't need to return a result, while Task<T> is used when you need a return value. Tasks can be awaited using the await keyword, allowing you to write asynchronous code sequentially. You can create tasks using Task.Run, Task.FromResult, or by using async/await methods. Tasks provide a cleaner abstraction than the older Thread class for asynchronous operations. The Task-based asynchronous pattern (TAP) is the modern approach recommended in .NET for asynchronous programming, replacing older patterns like APM and EAP.
Q38. What is async void? Why should you avoid it?
async void is an asynchronous method that doesn't return a Task. While it might seem convenient, it should be avoided in most cases because: (1) the caller cannot await the method or know when it completes, making it impossible to detect when the operation is done; (2) exceptions thrown in async void methods are difficult to catch and can crash the application; (3) you cannot track completion status for error handling or coordination; (4) it makes unit testing difficult. The only legitimate use case for async void is event handlers. For all other scenarios, use async Task instead, allowing the caller to await completion and handle exceptions properly. Using async Task provides better error handling, testability, and control flow compared to async void.
Q39. What is Threading in C#? What are its challenges?
Threading is a feature that allows multiple code paths to execute concurrently within a single application. The Thread class represents a thread of execution. Multithreading improves application responsiveness and can leverage multi-core processors for parallel processing. However, threading introduces significant challenges: (1) race conditions - multiple threads accessing shared data simultaneously; (2) deadlocks - threads waiting for resources held by each other; (3) complexity - concurrent code is harder to understand and debug; (4) synchronization overhead - protecting shared data requires locks and synchronization primitives. Modern alternatives like async/await and Task Parallel Library (TPL) are generally preferred over raw threading for most scenarios. If you need to use threads, use lock statements, Monitor, Mutex, or other synchronization mechanisms to protect shared data.
Q40. What is Thread Synchronization? Explain lock statement.
Thread synchronization ensures that only one thread can execute a critical section of code at a time, preventing race conditions and data corruption. The lock statement is the primary synchronization mechanism in C#, using a mutex to ensure that only one thread executes the locked code block at any time. When a thread enters a lock block, it acquires exclusive access to the object. Other threads attempting to acquire the same lock will block and wait. The lock statement is relatively simple but should be used judiciously as excessive locking can reduce concurrency and performance. More sophisticated synchronization mechanisms include ReaderWriterLockSlim (for scenarios with many readers and few writers), Semaphore, Mutex, and other signaling primitives. Modern concurrent collections in System.Collections.Concurrent provide thread-safe alternatives that often eliminate the need for explicit locking.
Example:
public class Counter
{
private int count = 0;
private object lockObject = new object();
public void Increment()
{
lock (lockObject)
{
count++; // Only one thread can execute this at a time
}
}
public int GetCount()
{
lock (lockObject)
{
return count;
}
}
}
Q41. What is String vs StringBuilder? When to use each?
Strings in C# are immutable - once created, they cannot be changed. Every string operation creates a new string object, which is inefficient when performing multiple string manipulations. StringBuilder, conversely, is mutable and maintains an internal buffer that can be modified without creating new objects. When you concatenate many strings or perform repeated modifications, StringBuilder is much more efficient. Use String when working with immutable string values or performing few operations. Use StringBuilder when you need to perform multiple string concatenations or modifications, especially in loops. The performance difference is negligible for a few operations, but significant when dealing with hundreds or thousands of string manipulations. StringBuilder is part of the System.Text namespace and should be your go-to choice for dynamic string building.
Q42. What is the difference between var and dynamic keywords?
var and dynamic are both used for implicit typing but work differently: (1) var - type is determined at compile-time by the compiler based on the assigned value. Once assigned, the variable has a fixed type and IntelliSense provides type information; (2) dynamic - type is determined at runtime, allowing you to change the type by assigning different values. No compile-time type checking occurs. Use var for static typing with automatic type inference when the type is clear from context. Use dynamic when you need true runtime type flexibility, such as working with COM objects, JSON parsing, or other dynamic scenarios. var is strongly typed and safer, while dynamic provides flexibility at the cost of safety and performance. In modern C#, var is preferred for most scenarios with dynamic reserved for specific interop or dynamic scenarios.
Q43. What is Pattern Matching in C#? Explain different pattern types.
Pattern matching is a feature that allows you to test whether a value matches a certain pattern and extract information if it does. C# supports several pattern types: (1) Type patterns - test if an object is of a specific type; (2) Constant patterns - match specific constant values; (3) Relational patterns - use comparison operators (<, >, <=, >=); (4) Logical patterns (and, or, not) - combine patterns; (5) Property patterns - match property values; (6) Positional patterns - deconstruct and match values. Pattern matching is used with switch expressions and when conditions, making code more expressive and readable. It eliminates the need for multiple if-else statements and type casts, resulting in cleaner code. Modern C# versions have significantly enhanced pattern matching capabilities, making it a powerful tool for writing concise and readable code.
Q44. What are recent C# features introduced in C# 8.0, 9.0, 10.0?
C# continues to evolve with new features: C# 8.0 introduced nullable reference types (improved null safety), async streams, using declarations, switch expressions, and default interface implementations. C# 9.0 introduced records (simplified immutable types), init-only properties, top-level statements, and covariant return types. C# 10.0 introduced file-scoped types (file keyword), record structs, required properties, global usings, and improvements to pattern matching. C# 11.0 introduced raw string literals (for SQL and JSON), generic attributes, UTF-8 string literals, and static abstract members in interfaces. C# 12.0 introduced primary constructors (simplified constructor syntax), collection expressions, and improvements to pattern matching. Each version aims to improve code clarity, reduce boilerplate, and enhance type safety. Staying updated with new features helps write more modern, efficient, and maintainable code.
Q45. What are Nullable Reference Types? How do they improve code safety?
Nullable reference types (introduced in C# 8.0) distinguish between reference types that can be null and those that cannot. By default (with #nullable enable), reference types cannot be null, and attempting to assign null or use null values produces compile-time warnings. You can explicitly mark a reference type as nullable using the "?" suffix (e.g., string?). This feature helps catch potential null reference exceptions at compile-time rather than at runtime. The compiler provides warnings when you: (1) assign null to non-nullable types; (2) dereference potentially null values; (3) access members of null values. This significantly improves code safety and reduces NullReferenceException bugs. Nullable reference types work alongside nullable value types, providing a comprehensive null-safety system. Adopting nullable reference types is a best practice for new projects and gradually enabling them in existing projects reduces bugs and improves reliability.
Example:
#nullable enable
public class Person
{
public string Name { get; set; } // Cannot be null
public string? NickName { get; set; } // Can be null
public void Greet()
{
Console.WriteLine($"Hello {Name}"); // Safe - Name is not null
// This would produce a warning without null check
if (NickName != null)
{
Console.WriteLine($"Also known as {NickName}");
}
}
}
Q46. What are Records in C#? How are they different from Classes?
Records (introduced in C# 9.0) are reference types designed for immutable data. They simplify the syntax for creating types focused on data rather than behavior. Records automatically provide: (1) constructors based on declared properties; (2) deconstruction support; (3) equality comparison based on content rather than reference; (4) useful ToString implementation. Records are particularly useful for data transfer objects (DTOs), value objects, and entities where immutability is important. Unlike classes, records have value-based equality - two records with the same property values are considered equal. Records support inheritance, allowing you to create record hierarchies. With init-only properties, records ensure that properties are set only during initialization. Records reduce boilerplate code significantly compared to classes, especially when you need immutable types with proper equality semantics. Records represent a shift toward functional programming patterns in C#.
Q47. What is Deconstruction? How do you implement it?
Deconstruction is a feature that extracts multiple values from an object and assigns them to separate variables in a single operation. You deconstruct an object using the syntax: (var x, var y) = obj. To enable deconstruction for a class, you implement a Deconstruct method with out parameters. Records automatically support deconstruction based on their properties. Deconstruction is useful for: (1) returning multiple values from methods without using tuples or classes; (2) extracting components from complex objects cleanly; (3) pattern matching in newer C# versions. Deconstruction improves code readability by allowing you to extract values concisely. It's particularly useful with tuples, which naturally support deconstruction for multi-value returns.
Q48. What are Tuples in C#? When are they useful?
Tuples are lightweight structures that hold multiple values of different types. They can be created using parentheses: (int, string, double) tuple = (42, "hello", 3.14). Named tuples allow you to assign names to elements: (int Id, string Name) person = (1, "John"). Tuples support deconstruction for easy value extraction. They are useful for: (1) returning multiple values from methods without creating a class; (2) temporary grouping of related data; (3) pattern matching in switch expressions. Tuples are value types, making them efficient for small data groups. They are less suitable for complex types or when you need methods and behavior - use classes or records instead. Tuples improved significantly with named elements in C# 7.1, making them more maintainable and readable.
Q49. What is readonly vs const - when to use each?
Both readonly and const define constants, but with important differences: (1) const values are compile-time constants with values hardcoded into the binary; (2) readonly values are runtime constants initialized at runtime (in constructor for instance readonly, in static constructor for static readonly). const is implicitly static and cannot be changed, while readonly can be either instance or static. Use const for known constant values (e.g., pi, max values), and readonly when: (1) the value is determined at runtime; (2) different instances need different "constant" values; (3) you need instance-level constants. readonly provides more flexibility and is safer for database lookups or configuration values that might change between application versions.
Q50. What is Reflection in C#? What are its use cases?
Reflection is the ability to inspect and interact with types, methods, properties, and other metadata at runtime. Using System.Reflection, you can: (1) get type information for any object; (2) invoke methods dynamically; (3) create instances of types at runtime; (4) read and set property values; (5) access custom attributes. Reflection is powerful but has performance implications and reduces type safety. Use cases include: (1) dependency injection frameworks that need to instantiate types dynamically; (2) ORM frameworks like Entity Framework that map objects to database tables; (3) serialization/deserialization libraries that work with any type; (4) testing frameworks that need to discover and execute test methods; (5) plugin systems that load assemblies at runtime. Reflection is essential for many framework features but should be used judiciously in application code due to performance and complexity trade-offs.