C# Academy · Lesson

Reusable generic utilities

Build tiny, reusable helpers: Guard checks, key-based equality comparer, memoization for pure functions, and a simple Result .

Lesson 3 of 38 steps

Reusable generic utilities is a free C# Academy lesson on CoddyKit. This is lesson 3 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 3 lessons in total.

Toolkit plan

Goal: Collect small helpers you can reuse across projects.

  • Guard checks for arguments
  • Key-based comparer for equality
  • Memoize a pure function
  • Result<T> for success/error flow

Guard checks

Centralize argument validation in a tiny Guard class; you call it at method start.

using System;

public static class Guard
{
  public static T NotNull<T>(T value, string name) where T : class
  {
    if (value == null) throw new ArgumentNullException(name);
    return value;
  }

  public static string NotNullOrEmpty(string value, string name)
  {
    if (string.IsNullOrEmpty(value)) throw new ArgumentException("must not be null or empty", name);
    return value;
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    // OK
    string name = Guard.NotNullOrEmpty("Ada", "name");
    Console.WriteLine("Hello, " + name);

    // Uncomment to see guard throw:
    // Guard.NotNull<string>(null, "input");
  }
}

Key-based comparer

Compare complex objects by a key (e.g., Id) without touching the model's Equals/GetHashCode.

using System;
using System.Collections.Generic;

public sealed class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
  private readonly Func<T, TKey> _keySelector;
  private readonly IEqualityComparer<TKey> _keyComparer;

  public KeyEqualityComparer(Func<T, TKey> keySelector)
    : this(keySelector, EqualityComparer<TKey>.Default) {}

  public KeyEqualityComparer(Func<T, TKey> keySelector, IEqualityComparer<TKey> keyComparer)
  {
    if (keySelector == null) throw new ArgumentNullException("keySelector");
    if (keyComparer == null) throw new ArgumentNullException("keyComparer");
    _keySelector = keySelector;
    _keyComparer = keyComparer;
  }

  public bool Equals(T x, T y)
  {
    if (object.ReferenceEquals(x, y)) return true;
    if ((object)x == null || (object)y == null) return false;
    return _keyComparer.Equals(_keySelector(x), _keySelector(y));
  }

  public int GetHashCode(T obj)
  {
    if ((object)obj == null) return 0;
    TKey key = _keySelector(obj);
    return key == null ? 0 : _keyComparer.GetHashCode(key);
  }
}

public sealed class User
{
  public int Id;
  public string Name;
}

public class Program
{
  public static void Main(string[] args)
  {
    List<User> list = new List<User>();
    list.Add(new User { Id = 1, Name = "Ada" });
    list.Add(new User { Id = 1, Name = "Ada Lovelace" }); // same Id -> considered equal
    list.Add(new User { Id = 2, Name = "Alan" });

    // Deduplicate by Id using a HashSet with our comparer
    HashSet<User> set = new HashSet<User>(new KeyEqualityComparer<User,int>(delegate(User u) { return u.Id; }));
    foreach (User u in list) set.Add(u);

    Console.WriteLine("Unique by Id = " + set.Count); // 2
  }
}

Memoization helper

Memoize caches results of a pure function. Use for repeat calls with the same input.

using System;
using System.Collections.Generic;

public static class Memo
{
  // Wrap a pure function f: K -> V with a simple cache
  public static Func<TKey, TValue> Memoize<TKey, TValue>(Func<TKey, TValue> f)
  {
    if (f == null) throw new ArgumentNullException("f");
    Dictionary<TKey, TValue> cache = new Dictionary<TKey, TValue>();
    return delegate(TKey key)
    {
      TValue v;
      if (cache.TryGetValue(key, out v)) return v;
      v = f(key);
      cache[key] = v;
      return v;
    };
  }
}

public class Program
{
  static int SlowSquare(int x)
  {
    // Simulate work
    System.Threading.Thread.Sleep(100);
    return x * x;
  }

  public static void Main(string[] args)
  {
    Func<int,int> fast = Memo.Memoize<int,int>(SlowSquare);

    var t1 = DateTime.UtcNow; Console.WriteLine(fast(12));
    var t2 = DateTime.UtcNow; Console.WriteLine(fast(12)); // from cache
    var dt1 = (t2 - t1).TotalMilliseconds;

    var t3 = DateTime.UtcNow; Console.WriteLine(fast(12)); // from cache again
    var t4 = DateTime.UtcNow; var dt2 = (t4 - t3).TotalMilliseconds;

    Console.WriteLine("First vs cached ms: " + dt1 + " / " + dt2);
  }
}

Result<T> wrapper

A minimal Result<T> type makes success/error explicit and easy to print or test.

using System;

public enum ResultKind { Ok, Error }

public sealed class Result<T>
{
  public ResultKind Kind;
  public T Value;            // valid when Ok
  public string Error;       // valid when Error

  private Result() {}

  public static Result<T> Ok(T value)
  {
    Result<T> r = new Result<T>();
    r.Kind = ResultKind.Ok;
    r.Value = value;
    r.Error = null;
    return r;
  }

  public static Result<T> Fail(string error)
  {
    if (string.IsNullOrEmpty(error)) throw new ArgumentException("error");
    Result<T> r = new Result<T>();
    r.Kind = ResultKind.Error;
    r.Error = error;
    r.Value = default(T);
    return r;
  }

  public override string ToString()
  {
    return Kind == ResultKind.Ok ? "OK: " + (Value == null ? "null" : Value.ToString())
                                 : "ERROR: " + Error;
  }
}

public class Program
{
  static Result<int> ParsePositiveInt(string s)
  {
    int n;
    if (!Int32.TryParse(s, out n)) return Result<int>.Fail("not a number");
    if (n <= 0) return Result<int>.Fail("must be > 0");
    return Result<int>.Ok(n);
  }

  public static void Main(string[] args)
  {
    Console.WriteLine(ParsePositiveInt("12"));
    Console.WriteLine(ParsePositiveInt("-1"));
    Console.WriteLine(ParsePositiveInt("x"));
  }
}

Practical tips

Tips:

  • Keep helpers tiny and focused.
  • Name them clearly: Guard, KeyEqualityComparer, Memo.
  • Document constraints and when to use (pure functions for Memo).
  • Write a quick unit test per helper.

Key comparer benefit

Quick check: What does a key-based IEqualityComparer let you do?

Recap

Recap: Reuse small tools—Guard for inputs, KeyEqualityComparer for equality, Memo for pure functions, and Result<T> for explicit outcomes.

Free to start

Learn C# with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
93
Lessons
346

Frequently Asked Questions

Is the “Reusable generic utilities” lesson free?

Yes — the full text of “Reusable generic utilities” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO. The C# Academy course includes 3 lessons in total.

What will I learn in “Reusable generic utilities”?

Build tiny, reusable helpers: Guard checks, key-based equality comparer, memoization for pure functions, and a simple Result . You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 3 of 3.

How long does the “Reusable generic utilities” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. unmanaged, notnull, new() (C# 6 emulation)
  2. Generic math (overview), generic attributes (when relevant)
  3. Reusable generic utilities
← Back to C# Academy