Use structures to improve the readability of your code

It's very common to use basic types such as string, int, or long to represent your data. However, sometimes there is a semantic behind the type. For instance, a string can be a file path, a full name, or an id. Also, you need to compare them or pretty-print the value. For instance, the length of a file is a long, but you almost never show the raw information. Instead, you format it as MB or GB. Which one do you prefer 507904 or 496kB? Moreover, you need to validate the data. For instance, the length of a file cannot be less than 0.

It's pretty easy to create a specific type when needed. If you don't want to create an overhead, you can use a struct.

public readonly struct FileLength
{
    public FileLength(long length)
    {
        Length = length;
    }

    public long Length { get; }
}

To make it easier to instantiate the FileLength, you can add implicit operators:

public readonly struct FileLength
{
    // ...

    public static implicit operator long(FileLength fileLength) => fileLength.Length;
    public static implicit operator FileLength(long fileLength) => new FileLength(fileLength);
}

Then, it's important to add equality and comparison operators if applicable. While the default equality operator may work as you want, it's important to override GetHashCode and Equals for performance reasons. You can read more about this in the post of Sergey Teplyakov. And, don't forget to implement interfaces such as IEquatable<T> or IComparable<T>.

Note that you can quickly generate equality operators using the quick fix:

Generate equality operators

Generate equality operators - Select members

public readonly struct FileLength : IEquatable<FileLength>, IComparable, IComparable<FileLength>
{
    // ...

    public override bool Equals(object obj) => obj is FileLength fileLength && Equals(fileLength);

    public bool Equals(FileLength other) => Length == other.Length;

    public override int GetHashCode() => Length.GetHashCode();

    public int CompareTo(FileLength other) => Length.CompareTo(other.Length);

    public int CompareTo(object obj)
    {
        var fileLength = (FileLength)obj;
        return CompareTo(fileLength);
    }

    public static bool operator ==(FileLength length1, FileLength length2) => length1.Equals(length2);

    public static bool operator !=(FileLength length1, FileLength length2) => !(length1 == length2);

    public static bool operator <=(FileLength length1, FileLength length2) => length1.CompareTo(length2) <= 0;

    public static bool operator >=(FileLength length1, FileLength length2) => length1.CompareTo(length2) >= 0;

    public static bool operator <(FileLength length1, FileLength length2) => length1.CompareTo(length2) < 0;

    public static bool operator >(FileLength length1, FileLength length2) => length1.CompareTo(length2) > 0;
}

Finally, it would be useful to add an override to the ToString method. If applicable you can implement IFormattable to support custom formats. Custom formats are useful to easily format the value in different contexts. You can always set the format in WPF, ASP.NET, string.Format, string interpolations.

public readonly struct FileLength : IFormattable
{
    // ...

    public override string ToString() => ToString(null, null);

    public string ToString(string format, IFormatProvider formatProvider)
    {
        if (string.IsNullOrEmpty(format))
            return Length.ToString(formatProvider);

        switch (format.ToUpperInvariant())
        {
            case 'KB':
                return (Length / 1024).ToString(formatProvider);

            case 'MB':
                return (Length / (1024 * 1024)).ToString(formatProvider);

            case 'GB':
                return (Length / (1024 * 1024 * 1024)).ToString(formatProvider);

            default:
                throw new ArgumentException("Format is invalid", nameof(format));
        }
    }

    // You can check the actual and much better implementation at
    // https://github.com/meziantou/Meziantou.Framework/blob/7a12229320d6833bfb34b5855b6035101cacbff2/src/Meziantou.Framework/FileLength.cs
}

Now you can use the FileLength type this way:

// Using the implicit converter to instantiate the FileLength
FileLength fileLength1 = new FileInfo("test1.txt").Length;
FileLength fileLength2 = new FileInfo("test2.txt").Length;

// Using the ToString with a format
Console.WriteLine($"File length: {fileLength:MB} MB");
Console.WriteLine("File length: {0:KB} KB", fileLength);
Console.WriteLine(string.Format("File length: {0:KB} KB", fileLength));

// Compare 2 instances
if (fileLength1 < fileLength2)
    Console.WriteLine("test1.txt is smaller than file2.txt");

// Use it in a class
public class CustomFileInfo
{
    public string FullPath { get; }
    public FileLength Length { get; }
    // ...
}

You can continue improving the type by implementing operators +, -, *, /, or a TypeConverter from easily convert the type from/to a string or any other types. ICustomTypeDescriptor can also be useful for binding the property to a property grid. You can add a DebuggerDisplay or DebuggerTypeProxy attribute to help debugging. If you are using Json.NET, you can add a JsonConverter. There are many ways to enrich your type. You'll find some examples in the .NET repository with types like TimeSpan or Index.

Creating types is not very complicated, but it makes you code clearer. These types are extensible. You can easily add new properties or methods to fill your needs. And in term of performance, using a struct versus an int, long, etc. is the same. So, there is no excuse for not adding more semantic to your code!

Leave a reply