0Pricing
C# Academy · Lesson

Reusable generic utilities

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

Reusable generic utilities is a free C# Academy lesson on CoddyKit — 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, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.

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.

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, and the C# Academy course includes 3 lessons in total. 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.

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; this is — lesson 3 of 3, so you can start here or from the beginning and move at your own pace.

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