The advent of C# 11 has brought with it a host of new features designed to improve and streamline the language’s functionality. One of these enhancements is the introduction of file-scoped types, a unique concept designed to prevent name collisions among types, especially in larger projects that utilize source generators.
Let’s take a closer look.
What Defines File-Scoped Types?
In C# 11, a file-scoped type refers to a top-level type that confines its scope and visibility to the file where we declare it. This is achieved using the file
keyword, acting as a type modifier. The file
modifier restricts a top-level type’s scope and visibility to the file in which it’s declared.
Let’s see how it works in practice:
namespace FileScopedTypesInCSharp; file class HiddenClass { public string Render() { return "Rendering file scoped Hidden Class"; } }
Here, the HiddenClass
class remains visible only within its file. This visibility allows other types in an assembly to adopt the name HiddenClass
without triggering a naming collision. To illustrate this, let’s use this type in a different file:
namespace FileScopedTypesInCSharp; public class Program { public static void Main(string[] args) { var hiddenClass = new HiddenClass(); var output = hiddenClass.Render(); Console.WriteLine(output); } }
Although both the Program
class and the HiddenClass
class are in the same namespace called FileScopedTypesInCSharp
, the code fails to compile with the error:
The type or namespace name 'HiddenClass' could not be found (are you missing a using directive or an assembly reference?)
However, if we move the HiddenClass
class to the Program.cs
file, the error disappears:
namespace FileScopedTypesInCSharp; public class Program { public static void Main(string[] args) { var hiddenClass = new HiddenClass(); var output = hiddenClass.Render(); Console.WriteLine(output); } } file class HiddenClass { public string Render() { return "Rendering file scoped Hidden Class"; } }
This code builds successfully and produces the desired output:
Rendering file scoped Hidden Class
Practical Use of File-Scoped Types
In C# 11, the new access modifier for file-scoped types specifically targets source generator authors. This feature lets authors define types confined to a single file, protecting these types from clashing with others in the same namespace and assembly. Code generators produce code dynamically during compilation, so anticipating the names of existing types in a user’s code base becomes challenging. Using file-scoped types, code generators can generate code that fits seamlessly into the user’s codebase without changing preexisting type names.
To further understand how they can be useful in practice, let’s define a source generator:
using System; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace SourceGenerator { [Generator] public class LoggingGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterImplementationSourceOutput( context.SyntaxProvider.CreateSyntaxProvider ( (syntaxNode, ct) => syntaxNode.IsKind(SyntaxKind.ClassDeclaration), (syntaxNode, ct) => syntaxNode ), GenerateLoggerClass ); } private void GenerateLoggerClass(SourceProductionContext sourceContext, GeneratorSyntaxContext context) { var classDeclaration = context.Node as ClassDeclarationSyntax; if (classDeclaration != null) { var className = classDeclaration.Identifier.Text; var sourceText = $$""" file class {className}Logger {{ public void Log(string message) {{ // Logging implementation omitted }} }} """; sourceContext.AddSource($"{className}Logger", SourceText.From(sourceText, Encoding.UTF8)); } } } }
In this example, the LoggingGenerator
examines each syntax tree in the compilation and identifies all user-declared classes. For each one, it generates a new file-scoped class, serving as that class’s logger. Since the logger class has file scope, it avoids conflicts with any of the user’s existing types. This approach lets the source generator produce code that blends seamlessly with the user’s codebase.
To learn more about source generators you can visit our article on the same topic.
Applicability of file Modifier
The file
modifier is not limited to classes; it can be applied to any top-level type:
- Class
- Interface
- Struct
- Enum
- Delegate
When a type is declared with the file
modifier, it becomes a file-scoped type.
Constraints of File-Scoped Types
File-scoped types offer significant advantages, but they have some limitations. For instance, we cannot assign other accessibility modifiers, such as public
and internal
to them. This restriction means we cannot declare a type as both file
and public
, or file
and internal
at the same time.
Declaring a file-scoped type in the same file under a different namespace makes it inaccessible. However, we can still access it by using its fully qualified name.
Lastly, any class implementing a file-scoped interface must also adopt a file scope. This rule ensures that classes restrict their implementation of an interface to the same file if that interface is only visible within that file.
Conclusion
File-scoped types represent a significant evolution in the C# language. By confining types to their files, this feature can prevent potential name collisions among types, which is particularly beneficial for large projects and source generators. While there are certain constraints, the benefits of file-scoped types far outweigh these minor limitations.