Using primitive types like string, int, or long to represent data is common, but these types often carry an implicit semantic. A string might be a file path, a full name, or an ID. Similarly, a file length is a long, but you rarely display the raw value; instead, you format it as KB or GB. Would you rather see 507904 or 496kB? You may also need to validate the data, for example, ensuring that a file length is never negative.
Creating a dedicated type for this is straightforward. To avoid heap allocation overhead, you can use a struct.
C#
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:
C#
public readonly struct FileLength
{
// ...
public static implicit operator long(FileLength fileLength) => fileLength.Length;
public static implicit operator FileLength(long fileLength) => new FileLength(fileLength);
}
Next, add equality and comparison operators where applicable. While the default equality operator may work correctly, you should override GetHashCode and Equals for performance reasons. You can read more about this in the post of Sergey Teplyakov. Also, consider implementing interfaces such as IEquatable<T> or IComparable<T>.
Note that you can quickly generate equality operators using the quick fix:
Quick action to generate equality operators
Select members for equality operators
C#
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, override ToString to provide a human-readable representation. You can also implement IFormattable to support custom format strings, which lets you control how the value is rendered in WPF bindings, ASP.NET, string.Format, and string interpolations.
C#
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:
C#
// 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 further enrich the type by implementing arithmetic operators (+, -, *, /), a TypeConverter for string conversion, or ICustomTypeDescriptor for property grid binding. Adding a DebuggerDisplay or DebuggerTypeProxy attribute helps with debugging. If you use Json.NET, consider adding a JsonConverter. For inspiration, look at well-known .NET types like TimeSpan or Index.
Creating dedicated types is simple, yet it makes your code significantly clearer and more expressive. These types are easily extensible with new properties and methods. In terms of performance, a struct has the same cost as a primitive like int or long. There is no reason not to add more semantic meaning to your code!
Do you have a question or a suggestion about this post? Contact me!