在.NET中如何表示仅有时间的值?

283

在.NET中,有没有一种方法可以表示仅包含时间值的内容?例如,指示商店开业时间?

TimeSpan 表示一个时间段,而我只想保存一个时间值。使用 DateTime 来表示这个值会导致新的 DateTime(1,1,1,8,30,0),这并不是真正想要的。

11个回答

193

你可以使用TimeSpan

TimeSpan timeSpan = new TimeSpan(2, 14, 18);
Console.WriteLine(timeSpan.ToString());     // Displays "02:14:18".

[编辑]
考虑到其他答案和问题的编辑,我仍然会使用TimeSpan。没有必要创建一个新结构,已有的框架足够使用。
在这些行上,您最终将复制许多本机数据类型。


24
没问题。DateTime使用TimeSpan恰好是为了这个目的。关于DateTime.TimeSpan属性的文档: "表示自午夜以来经过的一天的时间间隔的TimeSpan。" - Marcel Jackwerth
4
TimeSpan 表示一个时间间隔,而我所说的时间不是一个时间间隔,而是在一段日期范围内的一个单一固定点。 - sduplooy
3
它也可以用作固定点,正如你在问题中指定的那样,它没有日期。毕竟,你可以决定如何使用这些数据类型以使其更有益。 - John G
9
我同意原作者的观点,尽管 TimeSpan 可以用来表示固定点,但是过度使用会显得有些不雅。这是在框架内可用的最佳选择,但这并不意味着它很好用。 - Jon Skeet
7
从.Net 3.5版本开始,MSDN文档说明“TimeSpan结构也可以用来表示一天中的时间,但只有当时间与特定日期无关时才能使用。”换句话说,这正是提出问题的解决方案。 - Pharap
显示剩余18条评论

179

如其他人所说,您可以使用DateTime并忽略日期,或使用TimeSpan。个人而言,我对这两种解决方案都不太感兴趣,因为这两种类型都没有真正反映您尝试表示的概念 - 我认为.NET中的日期/时间类型有些稀疏,这也是我开始使用Noda Time的原因之一。在Noda Time中,您可以使用LocalTime类型来表示一天中的某个时间。

请注意,从.NET 6开始,有TimeOnlyDateOnly类型,它们与Noda Time的LocalTimeLocalDate类型大致相当。

需要考虑的一件事是:一天中的某个时间并不一定是自午夜以来的时间长度...

(另外需要注意的是,如果您还想表示商店的关闭时间,您可能会发现您想要表示24:00,即一天结束的时间。大多数日期/时间API - 包括Noda Time在内 - 都不允许将其表示为一天中的时间值。)


7
“一天的时间不一定是从午夜起算的时间长度…” 夏令时是唯一的原因吗?只是好奇您为什么没说清楚。 - jason
17
@Jason:日光节约时间是我能想到的唯一原因,忽略闰秒对大多数应用程序来说不相关。我主要是为了鼓励其他人思考为什么会这样才让它保持原样。我认为让人们更深入地思考日期/时间比他们目前所做的更重要 :) - Jon Skeet
1
@Oakcool:就像我在5月18日所说的那样:Noda Time中的“Duration”或BCL中的“TimeSpan”。我可能会将“视频中的位置+评论”封装为一种类型,然后有一个该类型的数组。 - Jon Skeet
1
感谢您更新并包含TimeOnly,但我认为它应该比一个附带的想法更加突出(完全理解您可能打算在后续编辑中这样做)... - TylerH
1
@TylerH:我想我们必须同意各自的观点 - 特别是我不认为TimeOnly是最好的方法,因为我仍然建议使用Noda Time - 如果您需要“时间”,那么您可能还需要其他日期/时间相关的概念,而我仍然相信Noda Time提供了最好的解决方案。我建议这个评论线程已经足够长了。 - Jon Skeet
显示剩余16条评论

38

如果空的Date让你感到困扰,你也可以创建一个更简单的Time结构:

// more work is required to make this even close to production ready
class Time
{
    // TODO: don't forget to add validation
    public int Hours   { get; set; }
    public int Minutes { get; set; }
    public int Seconds { get; set; }

    public override string ToString()
    {  
        return String.Format(
            "{0:00}:{1:00}:{2:00}",
            this.Hours, this.Minutes, this.Seconds);
    }
}

或者,为什么要费心:如果您不需要使用该信息进行任何计算,请将其存储为String

2
嗯...也许吧...但是为什么要重复造轮子呢?如果语言已经有了类/结构(C#和VB.NET就有),那就用它吧。但我理解你的回答想表达的意思。 - Kris Krause
4
由于已经存在TimeSpan可以更好地处理这个问题,因此我会给你投反对票。 - Noon Silk
1
@KrisKrause 我认为这不是重新发明轮子,TimeSpan 给你的远不止时间。如果仅将其用作时间,则可能会被误解和滥用,例如像 Days 这样的属性没有意义。 - Zaid Masud
21
+1 这比 TimeSpan 更好,因为它有较少的误解可能性... TimeSpan 真正意义上是用作间隔 (请参阅 MSDN),因此当 TimeSpan 用作时间时,像 Days 这样的属性就没有意义。 - Zaid Masud
1
虽然我认为拥有不同的类型是合理的,但我认为这不是一个好的解决方案:1)它是可变值类型;2)它不执行任何验证。 - Jon Skeet
显示剩余6条评论

21

我建议使用DateTime。如果不需要日期部分,可以忽略它。如果需要向用户显示时间,请按以下格式将其输出到用户界面:

DateTime.Now.ToString("t");  // outputs 10:00 PM

似乎制作新的类甚至使用TimeSpan的额外工作都是不必要的。

在这个方法中,你如何显示秒和毫秒? - Mona Jalal
5
毫秒: DateTime.Now.ToString("hh:mm:ss.fff"); 微秒: DateTime.Now.ToString("hh:mm:ss.ffffff"); 纳秒(如果DateTime具备那么高的分辨率):DateTime.Now.ToString("hh:mm:ss.fffffffff"); 根据MSDN - Pharap
2
因此,你认为为此实现正确的类型需要花费5到10分钟的时间比在整个代码库中考虑任何未来开发中DateTime属性可能只包含时间并且在这些情况下必须进行格式化,日期部分可能需要被忽略更费力吗?祝你在调试数据库、外部通信等出现"0001-01-01 10:00"的情况时好运...... - MarioDS

13

我认为Rubens的课程是一个好主意,所以想要创建他的Time类的不可变样本,并进行基本验证。

class Time
{
    public int Hours   { get; private set; }
    public int Minutes { get; private set; }
    public int Seconds { get; private set; }

    public Time(uint h, uint m, uint s)
    {
        if(h > 23 || m > 59 || s > 59)
        {
            throw new ArgumentException("Invalid time specified");
        }
        Hours = (int)h; Minutes = (int)m; Seconds = (int)s;
    }

    public Time(DateTime dt)
    {
        Hours = dt.Hour;
        Minutes = dt.Minute;
        Seconds = dt.Second;
    }

    public override string ToString()
    {  
        return String.Format(
            "{0:00}:{1:00}:{2:00}",
            this.Hours, this.Minutes, this.Seconds);
    }
}

您所添加的验证非常重要。TimeSpan类在建模时间方面的主要缺陷是一天内的时间可以超过24小时。 - shelbypereira
为什么小时、分钟和秒要使用 int 而不是 uint?如果没有理由,我认为它们可以直接使用 uint,这样可以避免在构造函数中进行转换。 - shelbypereira

8
C# 10 中,您可以使用 TimeOnly
TimeOnly date = TimeOnly.FromDateTime(DateTime.Now);

2
TimeOnly是一个特定于框架的功能。如果项目的目标是net6.0或更高版本,则可以在C# 9中使用该类型。 - Digital Coyote
1
据我所知,C#10 受 .Net 6 支持。@DigitalCoyote - Guillaume Raymond
@GuillaumeRaymond 没错,但是即使您使用 C# 9 作为语言版本,.Net 6 也支持 TimeOnly。您可以使用 C# 9 或更低版本来强制在针对旧框架的项目中链接文件的兼容性,或者允许编译器指令构建旧框架的应用程序。 - Digital Coyote

7

这是一个功能完整的TimeOfDay类。

对于简单情况来说,它可能有点过头,但如果你像我一样需要更高级的功能,那么这可能会有所帮助。

它可以处理边角案例、一些基本数学计算、比较、与DateTime的交互、解析等操作。

下面是TimeOfDay类的源代码。您可以在这里查看用法示例并了解更多信息:

这个类大部分使用DateTime进行内部计算和比较,以便我们可以利用DateTime中已经嵌入的所有知识。

// Author: Steve Lautenschlager, CambiaResearch.com
// License: MIT

using System;
using System.Text.RegularExpressions;

namespace Cambia
{
    public class TimeOfDay
    {
        private const int MINUTES_PER_DAY = 60 * 24;
        private const int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
        private const int SECONDS_PER_HOUR = 3600;
        private static Regex _TodRegex = new Regex(@"\d?\d:\d\d:\d\d|\d?\d:\d\d");

        public TimeOfDay()
        {
            Init(0, 0, 0);
        }
        public TimeOfDay(int hour, int minute, int second = 0)
        {
            Init(hour, minute, second);
        }
        public TimeOfDay(int hhmmss)
        {
            Init(hhmmss);
        }
        public TimeOfDay(DateTime dt)
        {
            Init(dt);
        }
        public TimeOfDay(TimeOfDay td)
        {
            Init(td.Hour, td.Minute, td.Second);
        }

        public int HHMMSS
        {
            get
            {
                return Hour * 10000 + Minute * 100 + Second;
            }
        }
        public int Hour { get; private set; }
        public int Minute { get; private set; }
        public int Second { get; private set; }
        public double TotalDays
        {
            get
            {
                return TotalSeconds / (24d * SECONDS_PER_HOUR);
            }
        }
        public double TotalHours
        {
            get
            {
                return TotalSeconds / (1d * SECONDS_PER_HOUR);
            }
        }
        public double TotalMinutes
        {
            get
            {
                return TotalSeconds / 60d;
            }
        }
        public int TotalSeconds
        {
            get
            {
                return Hour * 3600 + Minute * 60 + Second;
            }
        }
        public bool Equals(TimeOfDay other)
        {
            if (other == null) { return false; }
            return TotalSeconds == other.TotalSeconds;
        }
        public override bool Equals(object obj)
        {
            if (obj == null) { return false; }
            TimeOfDay td = obj as TimeOfDay;
            if (td == null) { return false; }
            else { return Equals(td); }
        }
        public override int GetHashCode()
        {
            return TotalSeconds;
        }
        public DateTime ToDateTime(DateTime dt)
        {
            return new DateTime(dt.Year, dt.Month, dt.Day, Hour, Minute, Second);
        }
        public override string ToString()
        {
            return ToString("HH:mm:ss");
        }
        public string ToString(string format)
        {
            DateTime now = DateTime.Now;
            DateTime dt = new DateTime(now.Year, now.Month, now.Day, Hour, Minute, Second);
            return dt.ToString(format);
        }
        public TimeSpan ToTimeSpan()
        {
            return new TimeSpan(Hour, Minute, Second);
        }
        public DateTime ToToday()
        {
            var now = DateTime.Now;
            return new DateTime(now.Year, now.Month, now.Day, Hour, Minute, Second);
        }

        #region -- Static --
        public static TimeOfDay Midnight { get { return new TimeOfDay(0, 0, 0); } }
        public static TimeOfDay Noon { get { return new TimeOfDay(12, 0, 0); } }
        public static TimeOfDay operator -(TimeOfDay t1, TimeOfDay t2)
        {
            DateTime now = DateTime.Now;
            DateTime dt1 = new DateTime(now.Year, now.Month, now.Day, t1.Hour, t1.Minute, t1.Second);
            TimeSpan ts = new TimeSpan(t2.Hour, t2.Minute, t2.Second);
            DateTime dt2 = dt1 - ts;
            return new TimeOfDay(dt2);
        }
        public static bool operator !=(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else
            {
                return t1.TotalSeconds != t2.TotalSeconds;
            }
        }
        public static bool operator !=(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 != dt2;
        }
        public static bool operator !=(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 != dt2;
        }
        public static TimeOfDay operator +(TimeOfDay t1, TimeOfDay t2)
        {
            DateTime now = DateTime.Now;
            DateTime dt1 = new DateTime(now.Year, now.Month, now.Day, t1.Hour, t1.Minute, t1.Second);
            TimeSpan ts = new TimeSpan(t2.Hour, t2.Minute, t2.Second);
            DateTime dt2 = dt1 + ts;
            return new TimeOfDay(dt2);
        }
        public static bool operator <(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else
            {
                return t1.TotalSeconds < t2.TotalSeconds;
            }
        }
        public static bool operator <(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 < dt2;
        }
        public static bool operator <(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 < dt2;
        }
        public static bool operator <=(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else
            {
                if (t1 == t2) { return true; }
                return t1.TotalSeconds <= t2.TotalSeconds;
            }
        }
        public static bool operator <=(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 <= dt2;
        }
        public static bool operator <=(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 <= dt2;
        }
        public static bool operator ==(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else { return t1.Equals(t2); }
        }
        public static bool operator ==(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 == dt2;
        }
        public static bool operator ==(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 == dt2;
        }
        public static bool operator >(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else
            {
                return t1.TotalSeconds > t2.TotalSeconds;
            }
        }
        public static bool operator >(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 > dt2;
        }
        public static bool operator >(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 > dt2;
        }
        public static bool operator >=(TimeOfDay t1, TimeOfDay t2)
        {
            if (ReferenceEquals(t1, t2)) { return true; }
            else if (ReferenceEquals(t1, null)) { return true; }
            else
            {
                return t1.TotalSeconds >= t2.TotalSeconds;
            }
        }
        public static bool operator >=(TimeOfDay t1, DateTime dt2)
        {
            if (ReferenceEquals(t1, null)) { return false; }
            DateTime dt1 = new DateTime(dt2.Year, dt2.Month, dt2.Day, t1.Hour, t1.Minute, t1.Second);
            return dt1 >= dt2;
        }
        public static bool operator >=(DateTime dt1, TimeOfDay t2)
        {
            if (ReferenceEquals(t2, null)) { return false; }
            DateTime dt2 = new DateTime(dt1.Year, dt1.Month, dt1.Day, t2.Hour, t2.Minute, t2.Second);
            return dt1 >= dt2;
        }
        /// <summary>
        /// Input examples:
        /// 14:21:17            (2pm 21min 17sec)
        /// 02:15               (2am 15min 0sec)
        /// 2:15                (2am 15min 0sec)
        /// 2/1/2017 14:21      (2pm 21min 0sec)
        /// TimeOfDay=15:13:12  (3pm 13min 12sec)
        /// </summary>
        public static TimeOfDay Parse(string s)
        {
            // We will parse any section of the text that matches this
            // pattern: dd:dd or dd:dd:dd where the first doublet can
            // be one or two digits for the hour.  But minute and second
            // must be two digits.

            Match m = _TodRegex.Match(s);
            string text = m.Value;
            string[] fields = text.Split(':');
            if (fields.Length < 2) { throw new ArgumentException("No valid time of day pattern found in input text"); }
            int hour = Convert.ToInt32(fields[0]);
            int min = Convert.ToInt32(fields[1]);
            int sec = fields.Length > 2 ? Convert.ToInt32(fields[2]) : 0;

            return new TimeOfDay(hour, min, sec);
        }
        #endregion

        private void Init(int hour, int minute, int second)
        {
            if (hour < 0 || hour > 23) { throw new ArgumentException("Invalid hour, must be from 0 to 23."); }
            if (minute < 0 || minute > 59) { throw new ArgumentException("Invalid minute, must be from 0 to 59."); }
            if (second < 0 || second > 59) { throw new ArgumentException("Invalid second, must be from 0 to 59."); }
            Hour = hour;
            Minute = minute;
            Second = second;
        }
        private void Init(int hhmmss)
        {
            int hour = hhmmss / 10000;
            int min = (hhmmss - hour * 10000) / 100;
            int sec = (hhmmss - hour * 10000 - min * 100);
            Init(hour, min, sec);
        }
        private void Init(DateTime dt)
        {
            Init(dt.Hour, dt.Minute, dt.Second);
        }
    }
}

7
除了Chibueze Opata之外,还有:
class Time
{
    public int Hours   { get; private set; }
    public int Minutes { get; private set; }
    public int Seconds { get; private set; }

    public Time(uint h, uint m, uint s)
    {
        if(h > 23 || m > 59 || s > 59)
        {
            throw new ArgumentException("Invalid time specified");
        }
        Hours = (int)h; Minutes = (int)m; Seconds = (int)s;
    }

    public Time(DateTime dt)
    {
        Hours = dt.Hour;
        Minutes = dt.Minute;
        Seconds = dt.Second;
    }

    public override string ToString()
    {  
        return String.Format(
            "{0:00}:{1:00}:{2:00}",
            this.Hours, this.Minutes, this.Seconds);
    }

    public void AddHours(uint h)
    {
        this.Hours += (int)h;
    }

    public void AddMinutes(uint m)
    {
        this.Minutes += (int)m;
        while(this.Minutes > 59)
            this.Minutes -= 60;
            this.AddHours(1);
    }

    public void AddSeconds(uint s)
    {
        this.Seconds += (int)s;
        while(this.Seconds > 59)
            this.Seconds -= 60;
            this.AddMinutes(1);
    }
}

你的分钟和秒钟的添加方法是错误的,因为它们没有考虑到超过59的值。 - Chibueze Opata
@Chibueze Opate:你说得完全正确。这只是一个快速而简单的代码。我应该在这段代码中再加些工作。稍后我会更新它...谢谢你的提示! - Jules

5

在即将发布的.NET 6版本中,最近批准了一个名为System.TimeOfDay的类型。

完整信息请参见https://github.com/dotnet/runtime/issues/49036

完成后,这将成为表示与任何特定日期或时区无关的时间值的首选方式。

System.TimeSpan仍然是表示经过时间值的推荐方式。


2
如果您不想使用DateTime或TimeSpan,只想存储一天中的时间,您可以将从午夜开始的秒数存储在Int32中,或者(如果您甚至不需要秒数),则从午夜开始的分钟数适合Int16。编写几个方法以从此类值访问Hour、Minute和Second将是微不足道的。
我能想到避免使用DateTime/TimeSpan的唯一原因是结构的大小很关键。
(当然,如果您使用上述简单方案包装在一个类中,那么如果您突然意识到这将给您带来优势,则将存储替换为TimeSpan也将是微不足道的)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接