Skip to main content

N# for C# Developers

You know C#. Here's the N# equivalent for everything you'll look up.

1. Variable Declaration

C#:

var name = "hello";
int count = 5;
const double Pi = 3.14;

N#:

name := "hello"
count: int = 5
let pi: double = 3.14

N# uses := for type inference (like Go), : type = for explicit types, and let for immutable bindings.

2. Function Declaration

C#:

public int Add(int a, int b)
{
return a + b;
}

private string FormatName(string first, string last)
{
return $"{first} {last}";
}

N#:

func Add(a: int, b: int): int {
return a + b
}

func formatName(first: string, last: string): string {
return $"{first} {last}"
}

No access modifiers — PascalCase = exported/public, camelCase = unexported/private-by-convention. Parameters use name: type syntax.

3. Class Definition

C#:

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
private string _id;

public Person(string name, int age)
{
Name = name;
Age = age;
_id = Guid.NewGuid().ToString();
}

public string GetInfo() => $"{Name}, {Age}";
}

N#:

class Person {
Name: string
Age: int
id: string

constructor(name: string, age: int) {
Name = name
Age = age
id = Guid.NewGuid().ToString()
}

func GetInfo(): string => $"{Name}, {Age}"
}

4. Properties

C#:

public string DisplayName => $"{First} {Last}";

public decimal Price
{
get => _price;
set
{
if (value < 0) throw new ArgumentException();
price = value;
}
}

N#:

DisplayName: string => $"{First} {Last}"

price: decimal
Price: decimal {
get => price
set {
if value < 0 {
throw new ArgumentException()
}
price = value
}
}

5. Constructors

C#:

public class Logger
{
private readonly string _name;

public Logger(string name)
{
_name = name;
}
}

// C# 12 primary constructor
public class Logger(string name)
{
public void Log(string msg) => Console.WriteLine($"[{name}] {msg}");
}

N#:

class Logger {
name: string

constructor(name: string) {
this.name = name
}
}

// Primary constructor
class Logger(name: string) {
func Log(msg: string) {
print $"[{name}] {msg}"
}
}

6. Interfaces

C#:

public interface IRepository<T>
{
Task<T?> GetByIdAsync(Guid id);
Task<List<T>> GetAllAsync();
}

public class UserRepo : IRepository<User>
{
public async Task<User?> GetByIdAsync(Guid id) { ... }
public async Task<List<User>> GetAllAsync() { ... }
}

N#:

interface IRepository<T> {
async func GetByIdAsync(id: Guid): T?
async func GetAllAsync(): List<T>
}

class UserRepo : IRepository<User> {
async func GetByIdAsync(id: Guid): User? { ... }
async func GetAllAsync(): List<User> { ... }
}

7. Inheritance

C#:

public class Animal
{
public virtual string Speak() => "...";
}

public class Dog : Animal
{
public override string Speak() => "Woof!";
}

N#:

class Animal {
virtual func Speak(): string => "..."
}

class Dog : Animal {
override func Speak(): string => "Woof!"
}

8. Generics

C#:

public class Stack<T>
{
public void Push(T item) { ... }
public T Pop() { ... }
}

public T Process<T>(T item) where T : IComparable
{
return item;
}

N#:

class Stack<T> {
func Push(item: T) { ... }
func Pop(): T { ... }
}

func Process<T>(item: T): T where T : IComparable {
return item
}

Generics are identical in capability — same <T> syntax, same constraints.

9. Async/Await

C#:

public async Task<string> FetchDataAsync(string url)
{
var client = new HttpClient();
return await client.GetStringAsync(url);
}

N#:

async func FetchDataAsync(url: string): string {
client := new HttpClient()
return await client.GetStringAsync(url)
}

Return types are automatically wrapped in Task<T> or ValueTask<T>. No need to write Task<string> in the signature — just write string.

10. LINQ

C#:

var results = items
.Where(x => x > 10)
.Select(x => x * 2)
.OrderBy(x => x)
.ToList();

N#:

results := items
.Where(x => x > 10)
.Select(x => x * 2)
.OrderBy(x => x)
.ToList()

LINQ is identical. Just drop the semicolons and var.

11. Pattern Matching

C#:

var result = value switch
{
0 => "zero",
> 0 => "positive",
_ => "negative"
};

N#:

result := match value {
0 => "zero",
> 0 => "positive",
_ => "negative"
}

match replaces switch expression. Same patterns (relational, type, property, list), different keyword.

12. Null Handling

C#:

string? name = null;
var display = name ?? "Unknown";
var length = name?.Length ?? 0;

N#:

name: string? = null
display := name ?? "Unknown"
length := name?.Length ?? 0

Same null-coalescing (??) and null-conditional (?.) operators. Same nullable types with ?.

13. String Interpolation

C#:

var msg = $"Hello, {name}! You are {age} years old.";
var formatted = $"Pi: {Math.PI:F2}";

N#:

msg := $"Hello, {name}! You are {age} years old."
formatted := $"Pi: {Math.PI:F2}"

Identical syntax.

14. Collections

C#:

var numbers = new[] { 1, 2, 3 };
var list = new List<int> { 1, 2, 3 };
var dict = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 87
};

N#:

numbers := [1, 2, 3]
let list: List<int> = [1, 2, 3]

dict := new Dictionary<string, int>()
dict["Alice"] = 95
dict["Bob"] = 87

Array literals use [...] without new. Collection expressions let [...] target List<T>, HashSet<T>, etc. when the type is explicit.

15. Error Handling

C#:

try
{
var result = DoThing();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

N#:

try {
result := DoThing()
} catch ex: Exception {
print ex.Message
}

The Go-style tuple trick — N#'s unique feature:

// Captures exception instead of throwing
result, err := DoThing()
if err != null {
print $"Error: {err.Message}"
}

Assign to two variables and the exception is caught automatically. This is like Go's result, err := f() pattern, but built into the language.

16. Enums

C#:

public enum Priority
{
Low = 0,
Medium = 1,
High = 2
}

// String enums? Nope. Workaround:
public static class Status
{
public const string Active = "active";
public const string Inactive = "inactive";
}

N#:

enum Priority {
Low = 0,
Medium = 1,
High = 2
}

// String enums — built in!
enum Status {
Active = "active",
Inactive = "inactive"
}

N# has native string enums. No more const string workarounds.

17. Records

C#:

public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = p1 with { Name = "Bob" };

N#:

record Person(name: string, age: int)

p1 := new Person("Alice", 30)
p2 := p1 with { name: "Bob" }

Records work the same — value equality, with expressions, immutability.

18. Unions (NEW)

C# doesn't have discriminated unions. N# does.

C# (manual approach):

public abstract class Result<T>
{
public sealed class Success : Result<T> { public T Value { get; init; } }
public sealed class Failure : Result<T> { public string Error { get; init; } }
}

N#:

union Result<T> {
Success { value: T }
Failure { error: string }
}

// Exhaustive matching — compiler ensures all cases handled
message := match result {
Result.Success { value } => $"Got: {value}",
Result.Failure { error } => $"Error: {error}"
}

Unions emit as idiomatic C# class hierarchies, so C# code can consume them naturally.

19. Duck Interfaces (NEW)

C# requires explicit interface implementation. N# adds structural typing.

N#:

duck interface IReader {
func Read(): string
}

// No ": IReader" declaration needed
class FileReader {
func Read(): string => "file contents"
}

class HttpReader {
func Read(): string => "http contents"
}

// Both work — they have the right shape
func process(r: IReader) {
print r.Read()
}

process(new FileReader()) // file contents
process(new HttpReader()) // http contents

If a type has the right methods, it satisfies the interface. Like Go interfaces, but on .NET.

20. Visibility

C#:

public class Service
{
public string Name { get; set; }
private int _count;
internal string ConnectionString { get; set; }
protected virtual void OnInit() { }
}

N#:

class Service {
Name: string // exported/public (PascalCase)
count: int // unexported/private-by-convention (camelCase)
internal ConnectionString: string
protected virtual func OnInit() { }
}

Convention-based: PascalCase = exported/public, camelCase = unexported/private-by-convention. Do not carry C# public/private into N# for ordinary code; the formatter removes redundant public/private when casing already says the same thing. Explicit modifiers are interop escape hatches that override casing, so public legacyCamel is exported and private SecretPascal is hidden; the formatter preserves those semantically necessary modifiers. Use explicit internal, protected, or file only for real .NET interop boundaries.

Quick Reference

TaskC#N#
Variablevar x = 5;x := 5
Explicit typeint x = 5;x: int = 5
Immutablereadonly / no reassignlet x := 5
Functionpublic int F(int a) { }func F(a: int): int { }
Private functionprivate void F() { }func f() { }
Classpublic class C { }class C { }
Propertypublic string X { get; set; }X: string
Constructorpublic C(string x) { }constructor(x: string) { }
Asyncasync Task<T> F()async func F(): T
Lambdax => x * 2x => x * 2
Arraynew[] { 1, 2, 3 }[1, 2, 3]
For-eachforeach (var x in items)for x in items { }
Ifif (x > 5) { }if x > 5 { }
Switch/Matchx switch { 0 => "a" }match x { 0 => "a" }
String interp$"Hi {name}"$"Hi {name}"
Importusing System;import System
Namespacenamespace X { }package X
Null coalescex ?? "default"x ?? "default"
Try/catchtry { } catch (Exception e) { }try { } catch e: Exception { }
Tuple errorN/Aresult, err := F()

Next Steps