C# 因在类型方面比 C++ 更严格而闻名。然而,我注意到没有人特别急于创建包装类型以提高可读性和易于转换。
例如,我正在编写代码,其中时间有时以秒为单位,有时以毫秒为单位,并且是恒定的,并且在可能的情况下,所有转换都是手动执行并乘以 1000。
我尝试创建两个类型为秒和毫秒的只读记录结构,从而重载转换运算符。我什至在其中包含了最大值的检查。
但我再次开始怀疑自己是否正确地花了两周的时间来完成这一切……为什么没有人这样做。我无法回答这个问题,但以下想法悄然浮现:
- 程序速度下降
- int/uint/long 的持续问题,我总是想节省空间并不断使用 int/uint/long 以确保它适合或使用负数
- 转换运算符的问题(我最近发现它们在 Sharp 中向后工作,并且我尝试解决这个问题的任何尝试都归结为每个人都写这是一个错误或一个功能这一事实。尽管我没有看到这有任何意义完全没有(实际上是这样的)
- 每个人都只是编写程序并最初使用 DateTime/TimeSpan - 然而,有时在几秒钟内为一个变量分配 8 个字节仍然是大胆的,特别是当一个类中有很多这样的变量并且经常使用时
一般来说,请分享您的经验。也许如果我从头开始编写程序,我就不会打扰。我遇到过由不同的人编写的项目 + 使用多个库,而这些库又具有自己的数据存储类型。有这样的油醋汁,我就坐下来,以为我会把所有的东西都清理干净。现在我越来越被怀疑所淹没,我简直陷入了恍惚之中:“我应该继续花时间在这上面,还是退缩而不受苦?”
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 - вот так захардкоржено и все)
Я бы начал с вопроса: зачем код то пишем?
Где же здесь точка опоры, которая скажет, как именно писать код? А нигде.
Поэтому разработчик при разработке опирается на следующее:
И здесь снова ничего про обёртки или экономию 8 байт памяти. Окей, давайте спустимся на низкий уровень и глянем на пример:
Вопрос: есть ли разница в использовании оперативной памяти этими двумя методами?
Ответ: нет, потому что эти 2 метода не используют оперативную память совсем.
Вот почему, взглянем на скомпилированный код:
Первый
Второй
Обращений к памяти нет. Поэтому далеко не всегда проблема, о которой вы говорите, существует в принципе.
Из всего выше сказанного есть один вывод: пишите понятный и поддерживаемый код. Не нужно абстракций ради абстракций или обёрток ради обёрток. Код должен легко читаться и легко дописываться.
Экономия 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.И строго везде
long, никаких интов и беззнаковых. На x64 системах в инте нет никакого смысла. Хотя повторюсь, я бы использовал простоDateTimeOffset, так как он и в таймштамп и из таймштампа напрямую конвертируется.Кстати, для универсальности и поддержки в коллекциях, я бы добавил реализацию интерфейсов
IComparable<T>иIEquatable<T>.Немутабельность конечно на ваше усмотрение, но снова: а надо ли? Присваивание и чтение
longв x64 атомарно.