C# Interface Declaration: Syntax and Usage
c# interface declaration: Learn the syntax and rules for declaring interfaces in C#, including members, access modifiers, and implementation patterns.
The C# interface declaration defines a contract that classes and structs can implement. It specifies the members that must be present without dictating how they are implemented. This article covers the syntax, member rules, and practical patterns for declaring interfaces in C#.
The Basic Syntax of an Interface Declaration
An interface is declared with the interface keyword followed by a name that typically starts with I. The body contains member signatures but no implementation.
public interface IShape { double Area { get; } void Draw(); }
This declares a contract that any implementing type must provide a read-only Area property and a Draw method. The interface itself contains no logic. It only describes what the implementing type must expose.
Interface Members and Their Implicit Rules
Interface members can be methods, properties, events, and indexers. Each member is implicitly public and abstract unless a default implementation is provided (see the section on default interface members). No fields, constructors, destructors, or static members are allowed in an interface declaration.
public interface IRepository { Task<T> GetByIdAsync<T>(int id); event EventHandler Changed; T this[int index] { get; set; } }
Property and indexer accessors are also part of the contract. An implementing class must provide the specified getter and setter behavior, though it can choose to implement them as expression-bodied members or with a backing field.
Access Modifiers and Visibility
The interface itself can have an access modifier such as public, internal, or private (for nested interfaces). Its members, however, cannot have access modifiers in the declaration. They are always public and abstract by default. Attempting to add private or protected to an interface member results in a compile-time error unless you are using default interface members with C# 8 or later.
internal interface ILogger { void Log(string message); // implicitly public }
The visibility of the interface controls where it can be referenced. An internal interface can be implemented by any type in the same assembly, but external code cannot see it.
Implementing an Interface in a Class
A class or struct implements an interface by declaring it in its base list and providing implementations for all required members. The implementing member must match the interface signature and be public unless you use explicit implementation.
public class Circle : IShape { public double Radius { get; set; } public double Area => Math.PI * Radius * Radius; public void Draw() { Console.WriteLine("Drawing a circle"); } }
The Circle class satisfies the IShape contract. The compiler enforces that every member declared in the interface is implemented. If you omit Draw(), the code will not compile.
Explicit Interface Implementation
When a class implements multiple interfaces that share a member signature, or when you want to hide an interface member from the class's public API, you can use explicit interface implementation. This is done by prefixing the member name with the interface name.
public interface IWriter { void Write(string text); } public interface ILogger { void Write(string text); } public class FileLogger : IWriter, ILogger { void IWriter.Write(string text) { // Write to file as a writer } void ILogger.Write(string text) { // Write to log as a logger } }
Explicitly implemented members are not accessible through the class instance; they can only be called through a reference to the interface. This is useful when the same method name has different meanings in different contracts.
Default Interface Members and Compatibility
Starting with C# 8, you can provide a default implementation for an interface member. This allows you to add new members to an interface without breaking existing implementers.
public interface IShape { double Area { get; } void Draw(); void Scale(double factor) { // Default implementation, does nothing } }
Existing classes that implement IShape do not need to implement Scale unless they want to override the default. However, default interface members are only available when the target runtime supports them (e.g., .NET Core 3.0+). On older runtimes, they cause a runtime failure. This feature is primarily for library evolution, not for everyday application code.
Interface Inheritance and Composition
Interfaces can inherit from one or more other interfaces. This allows you to build a contract from smaller, focused pieces.
public interface IReadable { string Read(); } public interface IWritable { void Write(string data); } public interface IReadWrite : IReadable, IWritable { }
A class that implements IReadWrite must implement all members from both base interfaces. This composition pattern encourages small, single-purpose interfaces rather than large monolithic ones.
Common Mistakes When Declaring Interfaces
One frequent mistake is using an interface to hold data rather than behavior. Interfaces are meant to define capabilities, not data structures. For example, an interface with only properties that represent a data record is often better modeled as a class or a record.
Another mistake is ignoring the naming convention. The I prefix is a widely accepted convention in C# and helps distinguish interfaces from classes. While not enforced by the compiler, it improves readability.
A third issue is declaring an interface with too many members. This forces every implementer to provide a large number of methods, even if they are not all relevant. Prefer splitting the interface into smaller, focused contracts.
Performance and Runtime Considerations
Interface calls are virtual calls. When you invoke a method through an interface reference, the runtime must resolve the actual implementation at runtime. This adds a small overhead compared to a direct call on a sealed class. In most applications, this overhead is negligible, but in hot paths it can matter.
The JIT compiler can sometimes devirtualize interface calls when it can prove the concrete type, but this is not guaranteed. If you are writing performance-critical code and know the concrete type, consider using a generic constraint with where T : IShape instead of passing an interface reference directly. This allows the JIT to generate more efficient code in some scenarios.
Another runtime consideration is boxing. When a struct implements an interface and is assigned to an interface variable, it is boxed. This allocates an object on the heap and copies the struct's data. Repeated boxing in a loop can cause significant memory pressure. If you need to use interfaces with structs, be aware of this cost and consider using generic methods to avoid boxing.
Finally, default interface members introduce a runtime dependency. If you use them, ensure your target framework supports them. Otherwise, you may encounter TypeLoadException at runtime even though the code compiles successfully.