Стоит ли создавать типы-обертки? в C# для удобочитаемости кода? (Тип с 1 полем)

C# славится тем, что он более строг к типизации, чем тот же С++. Однако я заметил, что особо никто не спешит создавать типы-обертки для удобочитаемости и простоты конвертации.

К примеру я работаю над кодом, в котором время то в секундах, то в милисекундах и постоянно и где только возможно все преобразования выполняются вручную с домножением на 1000.

Я попытался сделать два readonly record struct типа Seconds и Milliseconds, перегрузил операторы преобразования. Даже засунул в них проверку на максимальные значения.

Но вот опять же начинают терзать меня сомнения правильно ли я убил 2 недели на это все... Почему так никто не делает. Ответить на этот вопрос я не могу, но закрадываются такие мысли:

В общем, прошу поделиться опытом. Может если бы я писал свою программу с нуля - я бы не заморачивался. А я сталкиваюсь с проектами, которые писали разные люди + которые используют несколько библиотек, а те в свою очередь свои типы для хранения данных. Там такой винегрет, и я просто вот сел и думал, что все подчищу. А теперь меня все больше сомнения одолевают и я просто в ступоре: "Тратить ли дальше время на это или откатить назад и не мучаться?"

public readonly record struct Seconds
{
    public readonly int Ticks;
    public readonly static Seconds MaxValue = new Seconds(int.MaxValue);
    public readonly static Seconds MinValue = new Seconds(int.MinValue);
    public readonly static Seconds Zero = new Seconds(0);

    public Seconds(int ticks)
    {
        Ticks = ticks;
    }

    public static implicit operator int(Seconds time)
    {
        return time.Ticks;
    }

    public static explicit operator Seconds(int ticks)
    {
        return new Seconds(ticks);
    }

    public static implicit operator float(Seconds time)
    {
        return time.Ticks;
    }

    public static implicit operator TimeSpan(Seconds time)
    {
        return TimeSpan.FromSeconds(time.Ticks);
    }

    public static explicit operator Seconds(TimeSpan span)
    {
        var ticks = span.TotalSeconds;
        if (ticks < int.MinValue || ticks > int.MaxValue)
            throw new Exception("explicit operator Seconds(TimeSpan) : out of range value");
        return new((int)ticks);
    }

    public static explicit operator Seconds(Milliseconds time)
    {
        return new(time.Ticks / 1000);
    }

    public static explicit operator Seconds(RelativeTime span)
    {
        return new((int)(span.Milliseconds / 1000));
    }

    public static Seconds operator -(Seconds right)
    {
        return (Seconds)(-right.Ticks);
    }

    public static Seconds operator -(Seconds left, Seconds right)
    {
        return (Seconds)(left.Ticks - right.Ticks);
    }

    public static Seconds operator +(Seconds left, Seconds right)
    {
        return (Seconds)(left.Ticks + right.Ticks);
    }

    public static Seconds operator *(Seconds left, Seconds right)
    {
        return (Seconds)(left.Ticks * right.Ticks);
    }

    public static float operator /(Seconds left, Seconds right)
    {
        return (float)left.Ticks / right.Ticks;
    }

    public static Seconds operator %(Seconds left, Seconds right)
    {
        return (Seconds)(left.Ticks % right.Ticks);
    }

    public static bool operator >(Seconds left, Seconds right)
    {
        return left.Ticks > right.Ticks;
    }

    public static bool operator <(Seconds left, Seconds right)
    {
        return left.Ticks < right.Ticks;
    }

    public static bool operator >=(Seconds left, Seconds right)
    {
        return left.Ticks >= right.Ticks;
    }

    public static bool operator <=(Seconds left, Seconds right)
    {
        return left.Ticks <= right.Ticks;
    }

    public override string ToString()
    {
        return Ticks.ToString();
    }

    public static bool TryParse(string s, out Seconds result)
    {        
        bool isSuccessful = int.TryParse(s, out int parsed);
        result = (Seconds)parsed;
        return isSuccessful;
    }
}


public readonly record struct Milliseconds
{
    public readonly int Ticks;
    public readonly static Milliseconds MaxValue = new Milliseconds(int.MaxValue);
    public readonly static Milliseconds MinValue = new Milliseconds(int.MinValue);
    public readonly static Milliseconds Zero = new Milliseconds(0);

    public Milliseconds(int ticks)
    {
        Ticks = ticks;
    }

    public static implicit operator int(Milliseconds time)
    {
        return time.Ticks;
    }

    public static explicit operator Milliseconds(int ticks)
    {
        return new Milliseconds(ticks);
    }

    public static implicit operator float(Milliseconds time)
    {
        return time.Ticks;
    }

    public static implicit operator TimeSpan(Milliseconds time)
    {
        return TimeSpan.FromMilliseconds(time.Ticks);
    }

    public static explicit operator Milliseconds(TimeSpan span)
    {
        var ticks = span.TotalMilliseconds;
        if (ticks > int.MaxValue || ticks < int.MinValue)
            throw new Exception("explicit operator Milliseconds(TimeSpan) : out of range value");
        return new((int)ticks);
    }

    public static explicit operator Milliseconds(Seconds time)
    {
        var ticks = (long)time.Ticks * 1000;
        if (ticks > int.MaxValue || ticks < int.MinValue)
            throw new Exception("explicit operator Milliseconds(Seconds) : out of range value");
        return new Milliseconds((int)ticks);
    }

    public static explicit operator Milliseconds(RelativeTime span)
    {
        var ticks = span.Milliseconds;
        if (ticks > int.MaxValue)
            throw new Exception("explicit operator Milliseconds(RelativeTime) : out of range value");
        return new((int)ticks);
    }

    public static Milliseconds operator -(Milliseconds right)
    {
        return (Milliseconds)(-right.Ticks);
    }

    public static Milliseconds operator -(Milliseconds left, Milliseconds right)
    {
        return (Milliseconds)(left.Ticks - right.Ticks);
    }

    public static Milliseconds operator +(Milliseconds left, Milliseconds right)
    {
        return (Milliseconds)(left.Ticks + right.Ticks);
    }

    public static Milliseconds operator *(Milliseconds left, Milliseconds right)
    {
        return (Milliseconds)(left.Ticks * right.Ticks);
    }

    public static float operator /(Milliseconds left, Milliseconds right)
    {
        return (float)left.Ticks / right.Ticks;
    }

    public static Milliseconds operator %(Milliseconds left, Milliseconds right)
    {
        return (Milliseconds)(left.Ticks % right.Ticks);
    }

    public static bool operator >(Milliseconds left, Milliseconds right)
    {
        return left.Ticks > right.Ticks;
    }

    public static bool operator <(Milliseconds left, Milliseconds right)
    {
        return left.Ticks < right.Ticks;
    }

    public static bool operator >=(Milliseconds left, Milliseconds right)
    {
        return left.Ticks >= right.Ticks;
    }

    public static bool operator <=(Milliseconds left, Milliseconds right)
    {
        return left.Ticks <= right.Ticks;
    }

    public override string ToString()
    {
        return Ticks.ToString();
    }

    public static bool TryParse(string s, out Milliseconds result)
    {
        bool isSuccessful = int.TryParse(s, out int parsed);
        result = (Milliseconds)parsed;
        return isSuccessful;
    }
}

public readonly record struct RelativeTime
{
    public readonly uint Milliseconds;
    public readonly static RelativeTime MaxValue = new RelativeTime(uint.MaxValue);
    public readonly static RelativeTime MinValue = new RelativeTime(uint.MinValue);
    public readonly static RelativeTime Zero = new RelativeTime(0);

    public RelativeTime(uint milliseconds)
    {
        Milliseconds = milliseconds;
    }

    public static implicit operator uint(RelativeTime time)
    {
        return time.Milliseconds;
    }

    public static explicit operator RelativeTime(uint milliseconds)
    {
        return new RelativeTime(milliseconds);
    }    

    public static implicit operator float(RelativeTime time)
    {
        return time.Milliseconds;
    }

    public static RelativeTime operator -(RelativeTime left, RelativeTime right)
    {
        return (RelativeTime)(left.Milliseconds - right.Milliseconds);
    }

    public static RelativeTime operator +(RelativeTime left, RelativeTime right)
    {
        return (RelativeTime)(left.Milliseconds + right.Milliseconds);
    }

    public static RelativeTime operator +(RelativeTime left, Milliseconds right)
    {
        return (RelativeTime)(left.Milliseconds + right.Ticks);
    }

    public static RelativeTime operator -(RelativeTime left, Milliseconds right)
    {
        return (RelativeTime)(left.Milliseconds - right.Ticks);
    }

    public static bool operator >(RelativeTime left, RelativeTime right)
    {
        return left.Milliseconds > right.Milliseconds;
    }

    public static bool operator <(RelativeTime left, RelativeTime right)
    {
        return left.Milliseconds < right.Milliseconds;
    }

    public static bool operator >=(RelativeTime left, RelativeTime right)
    {
        return left.Milliseconds >= right.Milliseconds;
    }

    public static bool operator <=(RelativeTime left, RelativeTime right)
    {
        return left.Milliseconds <= right.Milliseconds;
    }

    public override string ToString()
    {
        return Milliseconds.ToString();
    }

    public static bool TryParse(string s, out RelativeTime result)
    {
        bool isSuccessful = uint.TryParse(s, out uint parsed);
        result = (RelativeTime)parsed;
        return isSuccessful;
    }
}

public readonly record struct UnixTime
{
    public readonly int Seconds;
    public static readonly UnixTime Zero = new UnixTime(0);
    public static readonly UnixTime Infinity = new UnixTime(-1);

    public UnixTime(int ticks)
    {
        if (ticks < 0 && ticks != -1)
            throw new Exception("constructor UnixTime(int) : too low value");
        Seconds = ticks;
    }

    public static implicit operator int(UnixTime time)
    {
        return time.Seconds;
    }

    public static explicit operator UnixTime(int seconds)
    {
        if (seconds < 0 && seconds != -1)
            throw new Exception("explicit operator UnixTime(int) : too low value");
        return new UnixTime(seconds);
    }

    public static explicit operator UnixTime(UnixTime64 time)
    {
        if (time.Seconds > int.MaxValue)
            throw new Exception("explicit operator UnixTime(UnixTime64) : too high value");
        return (UnixTime)(int)time.Seconds;
    }

    public static explicit operator UnixTime(UnixTimeMS time)
    {
        var seconds = time.Milliseconds / 1000;
        if (seconds > int.MaxValue)
            throw new Exception("explicit operator UnixTime(UnixTimeMS) : too high value");
        return (UnixTime)(int)seconds;
    }

    public static explicit operator DateTime(UnixTime time)
    {
        if (time == Infinity)
            return Time.Infinity;

        if (time == 0)
            return Time.Zero;

        return DateTimeOffset.FromUnixTimeSeconds(time).UtcDateTime;
    }

    public static explicit operator UnixTime(DateTime dateTime)
    {
        if (dateTime == Time.Infinity)
            return Infinity;

        if (dateTime == Time.Zero)
            return Zero;

        if (dateTime < DateTimeOffset.UnixEpoch.UtcDateTime)
            throw new Exception("explicit operator UnixTime(DateTime) : too low value");

        long ticks = new DateTimeOffset(dateTime).ToUnixTimeSeconds();

        if (ticks > int.MaxValue)
            throw new Exception("explicit operator UnixTime(DateTime) : too high value");
        return (UnixTime)(int)ticks;
    }

    public static Seconds operator -(UnixTime left, UnixTime right)
    {
        return (Seconds)(left.Seconds - right.Seconds);
    }

    public static UnixTime operator +(UnixTime left, Seconds right)
    {
        return (UnixTime)(left.Seconds + right.Ticks);
    }

    public static UnixTime operator -(UnixTime left, Seconds right)
    {
        return (UnixTime)(left.Seconds - right.Ticks);
    }

    public static bool operator >(UnixTime left, UnixTime right)
    {
        return left.Seconds > right.Seconds;
    }

    public static bool operator <(UnixTime left, UnixTime right)
    {
        return left.Seconds < right.Seconds;
    }

    public static bool operator >=(UnixTime left, UnixTime right)
    {
        return left.Seconds >= right.Seconds;
    }

    public static bool operator <=(UnixTime left, UnixTime right)
    {
        return left.Seconds <= right.Seconds;
    }
}

public readonly record struct UnixTime64
{
    public readonly long Seconds;
    public static readonly UnixTime64 Zero = new UnixTime64(0);
    public static readonly UnixTime64 Infinity = new UnixTime64(-1);

    public UnixTime64(long seconds)
    {
        if (seconds < 0 && seconds != -1)
            throw new Exception("constructor UnixTime64(long) : too low value");
        Seconds = seconds;
    }

    public static implicit operator long(UnixTime64 time)
    {
        return time.Seconds;
    }

    public static explicit operator UnixTime64(long seconds)
    {
        if (seconds < 0 && seconds != -1)
            throw new Exception("explicit operator UnixTime64(long) : too low value");
        return new UnixTime64(seconds);
    }

    public static explicit operator int(UnixTime64 time)
    {
        if (time.Seconds < 0 && time.Seconds != -1 || time.Seconds > int.MaxValue)
            throw new Exception("explicit operator int(UnixTime64) : out of range value");
        return (int)time.Seconds;
    }

    public static implicit operator UnixTime64(UnixTime time)
    {
        return (UnixTime64)time.Seconds;
    }

    public static explicit operator UnixTime64(UnixTimeMS time)
    {
        var seconds = time.Milliseconds / 1000;
        return (UnixTime64)seconds;
    }

    public static explicit operator DateTime(UnixTime64 time)
    {
        if (time == Infinity)
            return Time.Infinity;

        if (time == 0)
            return Time.Zero;

        return DateTimeOffset.FromUnixTimeSeconds(time).UtcDateTime;
    }

    public static explicit operator UnixTime64(DateTime dateTime)
    {
        if (dateTime == Time.Infinity)
            return Infinity;

        if (dateTime == Time.Zero)
            return Zero;

        if (dateTime < DateTimeOffset.UnixEpoch.UtcDateTime)
            throw new Exception("explicit operator UnixTime64(DateTime) : too low value");

        return (UnixTime64)new DateTimeOffset(dateTime).ToUnixTimeSeconds();
    }

    public static Seconds operator -(UnixTime64 left, UnixTime64 right)
    {
        return (Seconds)(left.Seconds - right.Seconds);
    }

    public static UnixTime64 operator +(UnixTime64 left, Seconds right)
    {
        return (UnixTime64)(left.Seconds + right.Ticks);
    }

    public static UnixTime64 operator -(UnixTime64 left, Seconds right)
    {
        return (UnixTime64)(left.Seconds - right.Ticks);
    }

    public static bool operator >(UnixTime64 left, UnixTime64 right)
    {
        return left.Seconds > right.Seconds;
    }

    public static bool operator <(UnixTime64 left, UnixTime64 right)
    {
        return left.Seconds < right.Seconds;
    }

    public static bool operator >=(UnixTime64 left, UnixTime64 right)
    {
        return left.Seconds >= right.Seconds;
    }

    public static bool operator <=(UnixTime64 left, UnixTime64 right)
    {
        return left.Seconds <= right.Seconds;
    }
}

public readonly record struct UnixTimeMS
{
    public readonly long Milliseconds;
    public static readonly UnixTimeMS Zero = new UnixTimeMS(0);
    public static readonly UnixTimeMS Infinity = new UnixTimeMS(-1);

    public UnixTimeMS(long milliseconds)
    {
        if (milliseconds < 0 && milliseconds != -1)
            throw new Exception("constructor UnixTimeMS(long) : too low value");
        Milliseconds = milliseconds;
    }

    public static implicit operator long(UnixTimeMS time)
    {
        return time.Milliseconds;
    }

    public static explicit operator UnixTimeMS(long milliseconds)
    {
        if (milliseconds < 0 && milliseconds != -1)
            throw new Exception("explicit operator UnixTimeMS(long) : too low value");
        return new UnixTimeMS(milliseconds);
    }

    public static explicit operator int(UnixTimeMS time)
    {
        if (time.Milliseconds < 0 && time.Milliseconds != -1 || time.Milliseconds > int.MaxValue)
            throw new Exception("explicit operator int(UnixTimeMS) : out of range value");
        return (int)time.Milliseconds;
    }

    public static implicit operator UnixTimeMS(UnixTime time)
    {
        long milliseconds = time.Seconds * 1000L;
        return (UnixTimeMS)milliseconds;
    }

    public static explicit operator UnixTimeMS(UnixTime64 time)
    {
        if (time.Seconds > long.MaxValue / 1000)
            throw new Exception("explicit operator UnixTimeMS(UnixTime64) : too high value");
        long milliseconds = time.Seconds * 1000L;
        return (UnixTimeMS)milliseconds;
    }

    public static explicit operator DateTime(UnixTimeMS time)
    {
        if (time == Infinity)
            return Time.Infinity;

        if (time == 0)
            return Time.Zero;

        return DateTimeOffset.FromUnixTimeSeconds(time).UtcDateTime;
    }

    public static explicit operator UnixTimeMS(DateTime dateTime)
    {
        if (dateTime == Time.Infinity)
            return Infinity;

        if (dateTime == Time.Zero)
            return Zero;

        if (dateTime < DateTimeOffset.UnixEpoch.UtcDateTime)
            throw new Exception("explicit operator UnixTimeMS(DateTime) : too low value");

        return (UnixTimeMS)new DateTimeOffset(dateTime).ToUnixTimeMilliseconds();
    }

    public static Seconds operator -(UnixTimeMS left, UnixTimeMS right)
    {
        return (Seconds)(left.Milliseconds - right.Milliseconds);
    }

    public static UnixTimeMS operator +(UnixTimeMS left, Seconds right)
    {
        return (UnixTimeMS)(left.Milliseconds + right.Ticks);
    }

    public static UnixTimeMS operator -(UnixTimeMS left, Seconds right)
    {
        return (UnixTimeMS)(left.Milliseconds - right.Ticks);
    }

    public static bool operator >(UnixTimeMS left, UnixTimeMS right)
    {
        return left.Milliseconds > right.Milliseconds;
    }

    public static bool operator <(UnixTimeMS left, UnixTimeMS right)
    {
        return left.Milliseconds < right.Milliseconds;
    }

    public static bool operator >=(UnixTimeMS left, UnixTimeMS right)
    {
        return left.Milliseconds >= right.Milliseconds;
    }

    public static bool operator <=(UnixTimeMS left, UnixTimeMS right)
    {
        return left.Milliseconds <= right.Milliseconds;
    }
}

public static class Time
{
    static Time()
    {
        ApplicationStartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime();
        Zero = DateTime.MinValue;
        Infinity = DateTime.MaxValue;
    }

    public static readonly DateTime ApplicationStartTime;
    public static readonly TimeSpan StartLocalOffset;

    public static readonly Seconds Minute = (Seconds)60;
    public static readonly Seconds Hour = (Seconds)(Minute * 60);
    public static readonly Seconds Day = (Seconds)(Hour * 24);
    public static readonly Seconds Week = (Seconds)(Day * 7);
    public static readonly Seconds Month = (Seconds)(Day * 30);
    public static readonly Seconds Year = (Seconds)(Month * 12);
    public static readonly Milliseconds InMilliseconds = (Milliseconds)1000;

    public static readonly DateTime Zero;
    public static readonly DateTime Infinity;

    /// <summary>
    /// Gets the current local time.
    /// </summary>
    public static DateTimeOffset NowLocal => DateTimeOffset.Now;

    /// <summary>
    /// Gets the current UTC time.
    /// </summary>
    public static DateTime Now => DateTime.UtcNow;

    /// <summary>
    /// Gets the application UpTime.
    /// </summary>
    public static TimeSpan UpTime => Now - ApplicationStartTime;

    /// <summary>
    /// Gets the application time relative to application start time in ms.
    /// </summary>
    public static RelativeTime NowRelative => (RelativeTime)UpTime.ToMilliseconds();

    /// <summary>
    /// Gets the difference to current UTC time.
    /// </summary>
    public static TimeSpan Diff(DateTime oldTime)
    {
        return Diff(oldTime, Now);
    }    

    /// <summary>
    /// Gets the difference to current UpTime.
    /// </summary>
    public static TimeSpan Diff(TimeSpan oldUpTime)
    {
        return UpTime - oldUpTime;
    }

    /// <summary>
    /// Gets the difference to current RelativeTime in milliseconds.
    /// </summary>
    public static Milliseconds Diff(RelativeTime oldMSTime)
    {
        return Diff(oldMSTime, NowRelative);
    }

    /// <summary>
    /// Gets the difference between two time points.
    /// </summary>
    public static TimeSpan Diff(DateTime oldTime, DateTime newTime)
    {
        return newTime - oldTime;
    }

    /// <summary>
    /// Gets the difference between two time spans.
    /// </summary>
    public static TimeSpan Diff(TimeSpan oldTimeSpan, TimeSpan newTimeSpan)
    {
        return newTimeSpan - oldTimeSpan;
    }

    /// <summary>
    /// Gets the difference between two relative to UpTime spans in milliseconds.
    /// </summary>
    public static Milliseconds Diff(RelativeTime oldMSTime, RelativeTime newMSTime)
    {
        if (oldMSTime > newMSTime)
            return (Milliseconds)((RelativeTime)0xFFFFFFFF - oldMSTime + newMSTime);
        else
            return (Milliseconds)(newMSTime - oldMSTime);
    }
}

И да, все эти типы используются и я немогу от них уйти. Часть в базе данных, часть в клиенте, часть в сервере.

Я понял что при передаче в функцию особо ничего не меняет 4 или 8 байт особенно учитывая х64 систему. Но есть в коде куча переменных с какимито таймерами или оперативными настройками, где хранится от 0 до 1 часа времени. Если сделать все 8 байт - размеры классов вырастут вдвое и втрое, а они все подвязаны к количеству клиентов онлайн. Так что расходы реально возрастают.

П.С. В коде проблема с 2038 годом решена тем, что в клиенте используется relative time 4байта. Экономия места как бы + оверфлов раз в месяц при стабильном аптайме (который правильно считает разницу). Естественно используется для какой-то ерунды. Там где нужно точно передать дату - используют 8байт, но UnixTime исключительно. Проблема же 2038 в коде сервера и базы данных пока не решена, но похоже пока это откладывается до 2038.

Показанный код - это все новое. В коде это просто int uint long + иногда комментарии или захардкоржено в имени переменной/метода. Распутываю до сих пор и нахожу даже баги, когда в sec пихали ms

И да, в самой базе данных все хранится в uint/ulong, а в коде используется все разнообразие. -1 => uint.MaxValue - вот так захардкоржено и все)


Ответы (1 шт):

Автор решения: aepot

Я бы начал с вопроса: зачем код то пишем?

  • Чтобы создать приложение, которое отвечает функциональным требованиям.
  • Чтобы оно было стабильным и отказоустойчивым.
  • Чтобы его можно было буквально не закапываясь в документацию, развивать и дорабатывать.

Где же здесь точка опоры, которая скажет, как именно писать код? А нигде.

Поэтому разработчик при разработке опирается на следующее:

  • SOLID - основные опорные принципы программирования
  • DRY - отказ от дублирования кода и источников данных
  • Типовые шаблоны проектирования, применимые к данному типу проекта

И здесь снова ничего про обёртки или экономию 8 байт памяти. Окей, давайте спустимся на низкий уровень и глянем на пример:

private long Increment(long value)
{
    return value + 1;
}

private int Increment(int value)
{
    return value + 1;
}

Вопрос: есть ли разница в использовании оперативной памяти этими двумя методами?

Ответ: нет, потому что эти 2 метода не используют оперативную память совсем.

Вот почему, взглянем на скомпилированный код:

Первый

lea rax, [rdx+1]
ret

Второй

lea eax, [rdx+1]
ret

Обращений к памяти нет. Поэтому далеко не всегда проблема, о которой вы говорите, существует в принципе.

Из всего выше сказанного есть один вывод: пишите понятный и поддерживаемый код. Не нужно абстракций ради абстракций или обёрток ради обёрток. Код должен легко читаться и легко дописываться.

Экономия 8 байт может вылиться в то, что спустя какое-то время вы не сможете (без допинга) разобраться в собственном коде.

Я видел много кода, и проблема, о которой вы говорите где-то в конце списка. И по частоте появления и по важности. А в начале списка вот что:

  • Злоупотребление конкатенациями и прочими модификациями строк, вызывающие кучу аллокаций и тормоза.
  • Безконтрольная аллокация других объектов, приводящая в ужас сборщик мусора.
  • Совершенно нечитабельный код типа if (x42 == 123) Func58(a, b, c, z);.
  • Хардкод данных. То что можно было вынести в настройки, лежит в коде.
  • Многократное использование одинаковых литералов вместо констант тоже весьма затрудняет, как чтение, так и отладку кода.

Поэтому гораздо эффективнее беречь разработчика, который будет читать ваш код, чем 8 байт памяти. Оптимизация - хорошо, переоптимизация - плохо. Оптимизировать бывает надо, но с профайлером и именно по проблемным местам, а не заранее начинать экономию на спичках, так как это может очень сильно увеличить сроки разработки при потенциально сомнительной пользе.

Конечно, вы правильно сделали, что добавили магии ради упрощения кода. Избавились от многократных умножений, это и есть реализация принципа DRY. С этим всё в порядке.


Посмотрел код, красиво и аккуратно сделано. А сколько из этой сотни методов реально используется? Половину точно выкинуть нельзя?

Хотя как по мне, я бы TimeSpan и DateTimeOffset использовал и не парился. Что касается таймштампа, то это просто int/long без какого-либо смысла заворачивать во что-либо, так как таймштампы для внешних коммуникаций или БД используются. А за это сериализатор/десериализатор отвечать должен, а не отдельная структура данных. Если у вас таймштампы по внутренней логике приложения гуляют, то что-то пошло не так, ну или есть специфика, о которой я не в курсе.

Ещё кстати есть DateOnly и TimeOnly. То есть в дотнете этого добра на любой вкус и цвет. Кстати, DateTime не люблю из-за его фокусов с часовыми поясами. Там очень аккуратно надо с ним играться.

Если уж очень хочется, я бы упростил - взял бы один класс UnixTime на базе long, сделал бы ему 2 свойства .Seconds и .Milliseconds и готово. Скрыл бы ему конструктор и сделал бы 2 статических FromSeconds и FromMilliseconds.

public class UnixTime
{
    private long _milliseconds;

    private UnixTime() { }

    public long Seconds
    {
        get => _milliseconds / 1000;
        set => _milliseconds = value * 1000;
    }

    public long Milliseconds
    {
        get => _milliseconds;
        set => _milliseconds = value;
    }

    public static UnixTime FromSeconds(long seconds)
    {
        return new() { Seconds = seconds };
    }

    public static UnixTime FromMilliseconds(long milliseconds)
    {
        return new() { Milliseconds = milliseconds };
    }

    public static UnixTime Now()
    {
        return FromMilliseconds(DateTimeOffset.Now().ToUnixTimeMilliseconds());
    }

    // дальше конверсии, операторы, соль, перец по вкусу.
    // а надо ли?
}

И строго везде long, никаких интов и беззнаковых. На x64 системах в инте нет никакого смысла. Хотя повторюсь, я бы использовал просто DateTimeOffset, так как он и в таймштамп и из таймштампа напрямую конвертируется.

Кстати, для универсальности и поддержки в коллекциях, я бы добавил реализацию интерфейсов IComparable<T> и IEquatable<T>.

Немутабельность конечно на ваше усмотрение, но снова: а надо ли? Присваивание и чтение long в x64 атомарно.

→ Ссылка