C#中的委托与事件

委托

关于委托

delegate 关键字创建的类型就是 System.MuticastDelegate (多播委托),使用反编译工具可以查看, System.MuticastDelegate 是保留类不允许继承。本质上委托都是多播委托,后期定义委托只有添加了多个方法才称为多播委托。

1
public delegate void NoReturnNoParametersDelegate();

常见实例化方式

1
2
3
4
5
6
//new关键字实例化
NoReturnNoParametersDelegate delegate1 = new NoReturnNoParametersDelegate(DoNothing);
//赋值一个方法
NoReturnNoParametersDelegate delegate2 = DoNothing;
//lambda表达式
NoReturnNoParametersDelegate delegate3 = () =>Console.WriteLine("This is a do nothing method.");

常见调用方式

1
2
3
delegate1.Invoke();//常规调用
delegate2();//像方法一样调用
delegate3.BeginInvoke(null, null);//异步调用

委托的意义

逻辑和行为作为参数来传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//公用的SQL执行方法
public T ExcuteSql<T>(string sql, Func<IDbCommand, T> func)
{
using(SqlConnection conn = new SqlConnection(""))
{
conn.Open();
IDbCommand cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.CommandType = CommandType.Text;
return func(cmd);
}
}
//执行查询
Func<IDbCommand, Student> func = (cmd) =>
{
IDataReader reader = cmd.ExecuteReader();
Student student = null;
if(reader.Read())
{
//对student赋值
student = new Student();
}
return student;
};
Student student = ExcuteSql<Student>("select top 1 * from [Student]", func);
//公用异常处理方法
public static void SafeInvoke(Action act)
{
try
{
act.Invoke();
}
catch(Exception exc)
{
Console.WriteLine(exc.Message);
}
}
//调用
SafeInvoke(() => ExcuteSql<Student>("select top 1 * from [Student]", func));

多播委托

  • += 就是在委托的实例后面增加注册更多的方法,像一个链子,执行的时候按添加顺序执行
  • -= 就是在委托的实例后面移除注册的方法,从链子的尾部开始匹配,找到一个完全匹配的移除,没有不异常
1
2
3
4
5
6
NoReturnNoParametersDelegate @delegate = DoNothing;
@delegate += DoNothing;
@delegate += DoNothing;
@delegate += DoNothing;
@delegate -= DoNothing;
@delegate.Invoke();

观察者模式

1
2
3
4
5
6
7
8
9
10
//观察者模式的标准做法就是将内容抽象 实现抽象接口方法DoAction() 添加到集合后遍历执行DoAction()方法
Action miaoAction = () => Console.WriteLine("Cat miao!");//多播委托做法
miaoAction += () => Console.WriteLine("Mourse run!");
miaoAction += () => Console.WriteLine("Dog wang!");
miaoAction += () => Console.WriteLine("Baby cry!");
miaoAction += () => Console.WriteLine("Brother turn!");
miaoAction += () => Console.WriteLine("Mother whisper!");
miaoAction += () => Console.WriteLine("Father roar!");
miaoAction += () => Console.WriteLine("Neighbor awake!");
miaoAction += () => Console.WriteLine("Thief hide!");

事件

事件定义

委托的实例,加一个 event 关键字(一组方法)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//定义一个事件
public class EventTestClass
{
//事件的发布者
public event Action TestEvent;
public void TestEventMethod()
{
Console.WriteLine("TestEventMethod invoke.");
TestEvent?.Invoke();//非空才执行
//if (TestEvent!=null)
//{
// TestEvent.Invoke();
//}
}
}

//事件注册
EventTestClass eventTest = new EventTestClass();
eventTest.TestEvent += () => Console.WriteLine("Cat miao!");//绑定方法 订户的订阅动作
eventTest.TestEvent += () => Console.WriteLine("Mourse run!");
eventTest.TestEvent += () => Console.WriteLine("Dog wang!");
eventTest.TestEvent += () => Console.WriteLine("Baby cry!");
eventTest.TestEvent += () => Console.WriteLine("Brother turn!");
eventTest.TestEvent += () => Console.WriteLine("Mother whisper!");
eventTest.TestEvent += () => Console.WriteLine("Father roar!");
eventTest.TestEvent += () => Console.WriteLine("Neighbor awake!");
eventTest.TestEvent += () => Console.WriteLine("Thief hide!");

//事件“EventTestClass.TestEvent”只能出现在 += 或 -= 的左边(从类型“EventTestClass”中使用时除外)
//eventTest.TestEvent = null;
//eventTest.TestEvent?.Invoke();

eventTest.TestEventMethod();

事件与委托的关系

委托是一种类型,事件是委托的一个实例。

event 关键字限制了权限,保证安全。

事件只能由发布者调用,外部不能调用也不能赋值,但是可以使用 +=-= 向事件注册方法。

这个流程可以理解为解耦

1
2
3
4
graph LR
A(发布者)-->|动作| B(订户A)
A-->|动作| C(订户B)
A-->|动作| D(订户C)

简单谈谈我对锤子及老罗的看法

不知不觉间,距上次锤子科技发布手机新品发布会已经接近一年了,而下一次感觉也是遥遥无期。手机行业,就是我这个外行人也知道,没有新品意味着什么 —— “死亡”。

老罗

其实开始我并不熟悉老罗,即便他曾经说出过像“彪悍的人生不需要解释”这种网络流行语。

我和大多数人认识老罗的过程是不一样的,好多人应该是认识老罗后,知道老罗去做手机,而我是在知道锤子手机后才认识的老罗。

我自认为自己一直是老罗的路人粉,自从看了他的锤子T1发布会之后。

那个时候还在武汉读书,从几次手机产品的发布,能真真切切的感受到他是想做出一款理想的产品,能够得到他人认可的产品。

而且可能是因为自己是一个工科男,所以对于其在系统、软件功能的创新,感觉特别的痴迷。

而现在,老罗这个名字,之前在各个社交网站论坛上,还能经常看到讽刺甚至谩骂他的文章,现在也慢慢少了,偶尔翻到只能是一声叹息。

手机

从锤子T1到坚果R1,几乎每场直播我都有追着看,一部分原因可能就像很多人一样,“看相声”。

比较遗憾的是,到目前为止,我也只买了一款坚果青春款,不过千元的手机把玩了一下,而后因为这个价格区间的性能问题,简单把玩一下就没再用送人了。

个人看来,就外观锤子手机还是很耐看的,每一款都有自己的特点。

但是性能和品控问题一直都有争议,比如我曾经买过的坚果刚买不久就发现信号不佳、音量键有点问题。

基本很少见到身边人用锤子手机的,记得有次出差和另外一个公司的实施人员接触,刚好他用的就是锤子手机,问了一下他的体验也是相比其他手机性能优化要差些,但是对于外观设计赞不绝口。但是也明确表示,下一部手机不会考虑锤子。

我自己最近也有想,锤子眼看着支持不下去了,无论是对于这段经历的一个纪念,还是补一张发布会的入场券,再买一部坚果。不过这个后面真的入手了再发开箱体验吧。

Smartisan OS

其实对于一个理工男,做软件开发的人来说,外观有时候并不是那么重要,我更看重的是“有趣的灵魂”。

说实话,在我看来,可能老罗不是一个合格的老板,但他绝对是一个优秀的产品经理。

一步、大爆炸、闪念胶囊、TNT,是我最看好的几个产品设计,作为效率工具来说设计新颖,改进的人机交互体验让人耳目一新。

说实话,发布会的产品介绍视频,外观那部分我感觉并不让我心动,但是系统和软件介绍我基本上都要翻来覆去看几遍。真的,有些设计点真的很戳心。

TNT工作站

在我看来,网上对于老罗的讽刺和谩骂,好像都是从“TNT”发布时的“李姐万岁”开始的。

看着这个产品的没落渐渐销声匿迹真的感觉非常遗憾,我也不止一次的在社交平台上发表过自己对于这个产品的看法,感觉很是惋惜。

我在B站上的一些回复:

文字工作者基本任务胜任我服,视频媒体呢,程序员呢,建立在这一套软硬件总是有瓶颈的吧,讲真老罗也就适合说相声,下次演讲就是海舟你来吧

5g时代配合云平台,手机不需要太好的性能也不需要做什么适配,复杂运算云服务搞定,还是很不错的选择。不是都说未来只需要一部计算机就行了吗?哈哈。

手机都不兼容 pc生态圈 他搞这个pc模式有什么意义啊…手机什么时候可以取代pc主机…在考虑这方面问题再说吧…

5g网络时代应该能联网就行,可以远程搞云服务器云计算,可以外接显示器,笔记本就没有用武之地了。挺看好这种模式的,就是不知道锤子能不能坚持到产品更新升级的那一刻。╮( ̄▽ ̄)╭

我在知乎上关于『如何评价锤子科技此次发布的坚果 TNT 工作站(显示器)?』的回答:

客观来说,适用的人群比较少,这个也是因为产品不够成熟,能够配套使用的办公软件更少。而且功能上有争议(我不建议用手机功能来对标PC,TNT这个功能应该凸显的是便携,而不是那个带触控的显示器,可能老罗太想卖产品了,其实只是作为一个idea可能不会那么多人喷)。

我是想如果一个手机,连接一个显示器就能够兼顾家用娱乐与商务办公,那真是省事不少。

作为一个经常出差的码农,希望手机可以运行ide环境(使用远程工具搭配云主机也是可以的),搭配一个投影设备或者出差地点提供的显示器工作,那真的是再好不过了。不过短期来看,不太现实,好像没有人向这个方向努力,老罗这一次让我看到了些许希望。

笔记本再便携,都不如手机方便,希望5G网络时代,能够让手机这种移动设备真正的替代桌面PC。(手机+云计算)

后记

对于老罗这个人,我也不做太多评论,但是网上一些网友的戾气是真的重,很多人都是为了黑而黑,逞口舌之快罢了。

而在我看来,去年的失利,就是老罗就是步子迈得太大。不知道是不是资金的压力,还是这么多年过去,产品销量上不去着急了,TNT显示器的发布可能是造成后续口碑严重下滑的一个重要因素。

看到老罗做聊天宝,卖行李箱、空气净化器……其实发布会能感觉到发自内心的不开心,为了资金资本,不能投入全部精力做自己喜欢的产品。听说最近又准备卖电子烟……

唉,看来真的是离做手机越来越远了啊。

C#中的反射

dll-IL-metadata-反射

C# 高级语言(人类语言) -> 编译器(编译) -> DLL/EXE (metadata(元数据)+IL(中间语言)) -> CLR/JIT -> 机器码(CPU 执行)

1
2
3
4
5
graph LR
A(C#高级语言) --> B(编译器)
B --> C(DLL / EXE)
C --> D(CLR / JIT)
D --> E(机器码)

反射

反射

反射是 .net framework 提供一个访问 metadata 的帮助类库,可以获取信息并使用。

使用

创建类型用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Animal
{
public string Name { get; set; }
public void Action()
{
Console.WriteLine("eat");
}
}
public class GenericClass<T1, T2>
{
public void Show(T1 t)
{
Console.WriteLine($"type is {typeof(T1).Name} value equals {t}");
}
}
public class Human : Animal
{
public Human(string name, string country)
{
this.Country = country;
this.Name = name;
}
public string Country { get; set; }
public static void Hobby()
{
Console.WriteLine("sleep");
}
public void SayHi()
{
Console.WriteLine($"My name is {this.Name}, I'm come from {this.Country}!");
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//引用命名空间
using System.Reflection;
//加载程序集
Assembly assembly1 = Assembly.Load("TestClassLibrary");//文件名
Assembly assembly2 = Assembly.LoadFile(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestClassLibrary.dll"));//文件路径
Assembly assembly3 = Assembly.LoadFrom(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestClassLibrary.dll"));//文件名或文件路径
//获取属性 模块 类型
assembly1.GetCustomAttributes();
assembly1.GetModules();
assembly1.GetTypes();
//获取指定类型
Type type1 = assembly1.GetType("TestClassLibrary.Human");//普通类
Type type2 = assembly1.GetType("TestClassLibrary.GenericClass`2");//泛型类
//泛型类或泛型方法使用前需要指定泛型类型
type2 = type2.MakeGenericType(typeof(string), typeof(string));//Method使用MakeGenericMethod方法
//实例化
object oSingleton1 = Activator.CreateInstance(type1, new object[] { "Kangkang", "China" });//有参数构造函数实例化
object oSingleton2 = Activator.CreateInstance(type2);//调用实例化方法入参需要传入实例化对象 静态方法可忽略
type1.GetMethod("Action").Invoke(oSingleton1, null);//实例化方法
type1.GetMethod("Hobby").Invoke(oSingleton1, null);//静态方法
type1.GetMethod("Hobby").Invoke(null, null);//静态方法
type1.GetMethod("SayHi").Invoke(oSingleton1, null);//实例化方法
type2.GetMethod("Show").Invoke(oSingleton2, new object[] { "test" });//泛型类有参数的方法

字段和属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Assembly assembly = Assembly.Load("TestClassLibrary");
Type type = assembly.GetType("TestClassLibrary.Human");
object oSingleton = Activator.CreateInstance(type, new object[] { "Kangkang", "China" });
var fields = type.GetFields();//获取字段
foreach (var item in fields)
{
Console.WriteLine($"name:{item.Name} value:{item.GetValue(oSingleton)}");
}
var properties = type.GetProperties();//获取属性
foreach (var item in properties)
{
Console.WriteLine($"name:{item.Name} type:{item.GetValue(oSingleton)}");//GetValue() 获取属性或字段值
}
type.GetProperty("Name").SetValue(oSingleton, "Xiaoming");//SetValue() 设置属性或字段值
type.GetMethod("SayHi").Invoke(oSingleton, null);

反射的优点和局限

缺点

  • 代码量较多书写麻烦
  • 性能消耗较大
  • 避开编译器检查

优点

  • 动态可拓展
  • 工厂模式
  • 分析类库文件
  • 访问不能访问的变量和属性破解第三方的代码

泛型(Part 2)

泛型约束

基类约束

可以访问基类的属性和方法,限制以后参数类型只能是该类型或其子类。

1
public static void GenericMethod<T>(T t) where T :GenericClass{}

接口约束

可以访问接口的属性和方法,限制以后参数类型必须有实现该接口。

1
public static void GenericMethod<T>(T t) where T : IGeneric{}

引用类型约束

限制参数类型只能是引用类型。

1
public static void GenericMethod<T>(T t) where T : class{}

值类型约束

限制参数类型只能是值类型。

1
public static void GenericMethod<T>(T t) where T : struct{}

无参数构造函数约束

限制参数类型可以被无参数构造函数实例化。

1
public static void GenericMethod<T>(T t) where T : new(){}

注:以上可以根据需求多重约束,叠加使用,其是“且”的关系。

1
public static void GenericMethod<T>(T t) where T : GenericClass,IGeneric{}

协变与逆变

使用场景

逆变(contravariant)与协变(covariant)是C#4新增的概念,在此之前泛型的参数是不能变化的,无论是“逆”还是“顺”(协)。

而在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。

  • 协变(covariant):out 修饰返回值。例如 Func<out T>
  • 逆变(contravariant):in 修饰传入参数。例如 Action<in T>

应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Bird{}

public class Sparrow:Bird{}
{
Bird bird1 = new Bird();
Bird bird2 = new Sparrow();
Sparrow sparrow1 = new Sparrow();
//Sparrow sparrow2 = new Bird(); //麻雀是鸟 但是鸟未必是麻雀
}

public interface ICustomerOut<out T>
{
T Get();//协变 只能作为返回值 不能作为传入参数
}

public interface ICustomerIn<in T>
{
void Set(T t);//逆变 只能作为传入参数 不能作为返回值
}

public class CustomerOut<T> : ICustomerOut<T>
{
public T Get()
{
return default(T);
}
}

public class CustomerIn<T> : ICustomerIn<T>
{
public void Set(T t)
{

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
List<Bird> listBird1 = new List<Bird>();
//List<Bird> listBird2 = new List<Sparrow>(); //不是父子关系,没有继承关系
List<Bird> listBird3 = new List<Sparrow>().Select(s => s as Bird).ToList();//一群麻雀一定是一群鸟
}

{//协变 只能作为返回值 不能作为传入参数
IEnumerable<Bird> listBird1 = new List<Bird>();
IEnumerable<Bird> listBird2 = new List<Sparrow>(); //协变 一群麻雀一定是一群鸟 类型转换由编译器执行
//IEnumerable<Sparrow> listBird3 = new List<Bird>();

ICustomerOut<Bird> customer4 = new CustomerOut<Bird>();
ICustomerOut<Bird> customer5 = new CustomerOut<Sparrow>();
//ICustomerOut<Sparrow> customer6 = new CustomerOut<Bird>();//接口应用

Action<int> action = (a) => { };//委托应用
}

{//逆变 只能作为传入参数 不能作为返回值
ICustomerIn<Bird> customer1 = new CustomerIn<Bird>();
//ICustomerIn<Bird> customer2 = new CustomerIn<Sparrow>();
ICustomerIn<Sparrow> customer3 = new CustomerIn<Bird>();//接口应用

Func<int> func = () => { return default(int); }; //委托应用
}

总结

逆变(in) 英语单词字面理解就是入参,而中文的字面意思就是逆向的即从子向父转换,可以参考系统提供的委托 Action<T>

Action<T> 委托这样使用可以理解为一个已经限制了入参为派生类的委托,那么这个委托将不能赋值给入参为基类型的委托。也就是说如果已经限制了一个委托的入参类型,那么这个委托的入参只能是该类型或该类型的派生类型,否则会影响委托内方法、属性等的调用。所以反之我们可以将一个入参为基类型的委托赋值给一个入参是派生类型的委托。

1
2
3
4
5
6
7
8
9
10
11
12
13
// public delegate void Action<in T>(T obj);
Action<Animal> actAnimal = (animal) =>
{
animal.Eat();
//animal.Meow();
};
Action<Cat> actCat = (cat) =>
{
cat.Eat();
cat.Meow();
};
// actAnimal = actCat; //报错:猫咪会喵喵叫,但是不是所有动物都会喵喵叫。
actCat = actAnimal;

协变(out) 英语单词字面理解就是出参,而中文的字面意思就是正向(协有顺的意思)的即从父向子转换,可以参考系统提供的委托 Func<T>

Func<T> 委托这样使用可以理解为一个已经限制返回值为基类型的委托,那么这个委托将不能赋值给返回值为派生类的委托。也就是说已经限制了一个委托的返回值类型,那么这个委托的返回值只能是该类型或该类型的基类型,因为我们并不能确定该返回值的类型是否是该派生类型。所以反之我们可以将返回值为派生类的委托赋值给一个返回值为基类型的委托。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// public delegate TResult Func<out TResult>();
Func<Animal> funcAnimal = () =>
{
Animal animal = null;
if (new Random().Next() % 2 == 0)
{
animal = new Animal();
}
else
{
animal = new Cat();
}
return animal;
};
Func<Cat> funcCat = () =>
{
Cat cat = null;
if (new Random().Next() % 2 == 0)
{
//cat = new Animal();
}
else
{
cat = new Cat();
}
return cat;
};
funcAnimal = funcCat;
// funcCat = funcAnimal;//报错:猫咪是动物,但是动物未必是喵咪。

如果感觉协变和逆变的内容难以理解,可以参看博客园的文章逆变与协变详解,实际我们只需要了解基本概念以及学会如何使用即可,对于内部的实现原理不必深究。

泛型缓存

因为每一个泛型类型,都会生成不同副本,适合不同类型,需要缓存一份数据的场景。而无论是线程安全性还是检索速度,泛型缓存都要比字典类型表现更佳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GenericCache<T>
{
static GenericCache()
{
GetCache();
}

public GenericCache()//实例化 相当于手动更新缓存
{
_cache = null;
}

private static List<T> _cache = null;

public static List<T> GetCache()
{
if (_cache == null)
{
//为缓存赋值 若需要实时更新 可以自己设置策略检查更新是否过期
//_cache = DbHelper.GetAll<T>();
}
return _cache;
}
}

泛型 (Part 1)

什么是泛型

起始版本为 .net framework 2.0 CLR升级 支持的,非语法糖。为解决针对不同参数类型,有相同的操作行为。

  • 延迟声明:声明方法的时候并没有指定参数的类型,而是等调用的时候指定。
  • 延迟思想:推迟一切可以推迟的。(提高程序的灵活性和扩展性)

打印泛型集合和字典:

1
2
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));

输出内容

1
2
System.Collections.Generic.List\`1[T]
System.Collections.Generic.Dictionary\`2[TKey,TValue]

编译的时候,类型参数被编译为占位符,程序运行时,JIT即时编译为是类型,所以其性能近似普通方法。

泛型方法

  • 定义:MethodName<T>(T t){} 其中T是类型参数,名称不做限制。
  • 使用:MethodName<int>(123)MethodName(123) 当类型参数可以由编译器推断时,可以不指定参数类型。

普通方法

优点是直观、性能较好;缺点为相同逻辑不能重用,拓展性差。

1
2
3
4
5
6
7
8
9
10
11
12
public static void ShowInt(int i)
{
Console.WriteLine($"type:{i.GetType()}\tvalue:{i}");
}
public static void ShowString(string s)
{
Console.WriteLine($"type:{s.GetType()}\tvalue:{s}");
}
public static void ShowDateTime(DateTime dt)
{
Console.WriteLine($"type:{dt.GetType()}\tvalue:{dt}");
}

object 方法

优点是基类型可以用子类代替,解决相同逻辑复用问题;缺点是存在拆装箱,影响性能。

1
2
3
4
public static void ShowObject(Object o)
{
Console.WriteLine($"type:{o.GetType()}\tvalue:{o}");
}

泛型方法

可以复用,同时不存在性能问题。

1
2
3
4
public static void Show<T>(T t)
{
Console.WriteLine($"type:{t.GetType()}\tvalue:{t}");
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
//普通方法
CommonMethod.ShowInt(123);
CommonMethod.ShowString("123");
CommonMethod.ShowDateTime(DateTime.Now);
//object方法
CommonMethod.ShowObject(123);
CommonMethod.ShowObject("123");
CommonMethod.ShowObject(DateTime.Now);
//泛型方法
CommonMethod.Show(123);
CommonMethod.Show("123");
CommonMethod.Show(DateTime.Now);

输出内容

1
2
3
4
5
6
7
8
9
type:System.Int32   value:123
type:System.String value:123
type:System.DateTime value:2018/5/1 20:14:33
type:System.Int32 value:123
type:System.String value:123
type:System.DateTime value:2018/5/1 20:14:33
type:System.Int32 value:123
type:System.String value:123
type:System.DateTime value:2018/5/1 20:14:33

注:object是一切类型的父类;通过继承,子类拥有父类的一切属性和行为;任何子类出现的地方,都可以用父类来代替;值类型转换为引用类型是装箱,引用类型转换为值类型是拆箱。

其他使用场景

除泛型方法以外,还可以使用泛型类、泛型接口、泛型委托。

1. 泛型类

1
2
3
4
5
6
7
8
public class GenericClass<T1, T2>
{
public void Show<T1>(T1 t1){ }
public T2 Get()
{
return default(T2);
}
}

2. 泛型接口

1
2
3
4
public class IGeneric<T>
{
T IMethod(T t);
}

3. 泛型委托

1
public delegate T GetHandler<T>();

注:普通类不能直接继承自泛型类也不能直接实现泛型接口,指定参数类型后才可以。泛型类可以继承泛型类。

1
2
3
public class GenericClass<T1, T2>{}
public class Child: GernericClass<int, string>, IStudy<DateTime>{}
public class ChildGeneric<T1, T2>: GenericClass<T1, T2>{}

免责声明

浏览者在访问本站前,请务必阅读本文以下条款并同意以下声明:

  1. 华灯博客(以下简称本站)所有原创文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处,否则本站有权依法追究其法律责任。

  2. 关于本站的所有留言评论、引用文纯属文字原作者个人观点,与本站观点及立场无关;浏览者在本站发表信息时需要遵守中国现行相应法律,不得发布违法信息。

  3. 本站部分内容来自互联网,若无意间侵犯到媒体、公司、企业或个人的知识产权,请及时邮件告知,本站将及时处理。

  4. 本站转载的部分内容,如有您未署名的作品,请及时邮件联系,本站会及时为您的作品做署名及相关处理。

以上声明内容的最终解释权归华灯博客

常用工具或软件推荐

系统工具

PE工具:微PE —— 良心PE,情怀PE,没有广告而且很容易得到。

文件搜索:Wox —— 搜索体验比微软小娜好,支持拼音,搭配Everything简直无敌!

文件搜索:Everything —— 将磁盘文件建立索引后,搜索极快。

串口模拟:Virtual Serial Port Driver —— 简单易用,串口开发调试必备。

远程工具:Team Viewer —— 远程体验极好,甩QQ远程N条街。

关闭Win10自动更新: Anti-Windows-Update —— 妈妈再也不怕Win10强制自动更新,而且使用以后仍可以手动更新。

压缩工具:7zip —— 这个必须吹爆,体验最好的压缩解压缩工具没有之一。

云端存储:OneDrive —— 购买Office 365一个最主要原因就是它。

截图录屏:FastStone Capture —— 虽然不是使用体验不是最佳,但是功能最强。兼具截图、录屏、屏幕放大、拾色、标尺等功能,截图可以绘制,录屏可以转gif。

编程工具

必备IDE:Visual Studio —— 这个dotnet开发必备,没得解释,宇宙第一IDE不是吹的。

必备IDE:Visual Studio Code —— 稳定排名第一,就是好用。

必备IDE:Notepad++ —— 轻量级,小身材大能量。

文本比较工具:BComapare —— 功能全面,不仅可以对比文件,还可以对比文件夹压缩文件等。

反编译工具:ILSpy —— 绿色易用,dotnet反编译必备。

抓包工具:Fiddler —— 这个强推,使用特别方便。

数据库管理:PL/SQL —— Oracle数据库最佳的伙伴,推荐直接使用12版,支持x64。

数据库管理:SQLite Studio —— 绿色易用。

数据库管理:Nvicate Primium —— 全能数据库管理工具,支持各种数据库。

学习娱乐

阅读工具:Kindle —— 还是要多看看书,搭配Kindle Unlimited食用最佳。

效率工具:有道云笔记 —— 除了检索有点慢,其他体验还算可以,可以使用Markdown,但是个人感觉还有待提高。

浏览器插件

编程必备辅助工具:FeHelper(Web前端助手) —— 支持Json序列化、代码压缩、代码美化、二维码生成等等常用功能。

翻译工具:Google Translate —— 翻译比较准确,特别是一些专业名词。

网页调试:Postman —— 这个基本都知道,需要模拟Http请求基本都会用到。

暂时就这些,想到再更。

我与培训机构的二三事

对于培训机构的态度

在武汉读书的时候,由于经常摸鱼,所以专业课学的挺差,印象很深的是一次专业课老师提问,小伙伴们疯狂输出最后自己还是答不上来,印象中只能在电脑前傻笑,现在回想真的像是一个傻子。

有时候也会问自己,为什么自己要来这个地方,选择这么一个专业,但是当时心态和环境是很难看进去书的,本身自学能力又不是特别好,于是开始从网上搜索一些与专业相关的视频来看。

所以,基于这段经历,对于大多数培训机构,我是持积极态度去看待的,因为我自己,就是凭借传智播客在网上的一些公开课视频入门并喜欢上编程。

传智播客

印象中,传智播客的.NET培训在当时很热门,而且他们本身提供了所谓基础班的视频,所以当时下载下来,从Hello world!都有一直往后看。

我很认同的一个观点是,培训机构的快餐式教育并不适合所有人,我们可以通过培训机构了解某一个学科,但是真正的熟悉并掌握,还是要靠自己的努力去积累并运用。

在这一点我要感谢培训视频中的那个老师,他在视频中一直强调的也是多看、多写、多实践。

所以我自己在学完基础班视频以后,相信培训班能带领我们入门,可以教我们学习的方式方法,但最终还是要靠自己自学,才能成长成才。

(所以我没有去报名那一万多的就业班,不知道老师讲这些话的时候有没有后悔,但是印象中后来传智播客的.NET培训班就慢慢没落直至不再办学。)

软谋教育

软谋教育是我入职一年后接触的,那段时间真的很迷茫。

虽然毕业前抓紧时间学习了一些基础知识,能够顺利的找到一份实习工作并完成就业,但是实际上自己的能力还是不足,所以自己进了一家企业的“工程部门”,简而言之就是改BUG部门。

公司设置有产品研发中心开发产品,而我只是在这个的基础上完善、或者捉虫,这其实并不好玩,并且发现自己之前学习的知识也慢慢开始淡忘,这段时间经常感觉自己很快会被淘汰,因为技术是在进步的,而我非但不能进步反而之前掌握的知识也在长期得不到运用的情况下渐渐遗忘。

Web全栈培训班

最开始是无意间在QQ上看到了腾讯课堂的推送,当时迷茫的自己首先看到的是软谋的Web全栈培训班,那时候.net core还没有火起来,所以感觉全栈也许未来一个热门的就业方向,于是花了两千多报名了VIP班。(最主要是便宜真香)

就和之前在看传智播客入门视频时一样,本想借这个机会入门,但是最终发现自己并不喜欢前端,只是学了点皮毛就没有继续学习下去的兴趣,而且实际上所谓的全栈班学习到的后端知识也只是皮毛,所以后来也没提起兴趣继续学习下去。

.NET 高级班

就这样在软谋教育的QQ群游荡了一段时间,偶然间看到了他们推广的.NET 高级班,当时那种状态下,他们的课表一下点燃了自己学习的热情,遂又破财报名了VIP班。

说实话,到现在都非常感谢Eleven老师,教学态度非常认真,知识点剖析的也非常细致,虽然当时工作忙没有将课程全部追上看完,但是课下也基本都补了上来,受益良多。

比较遗憾的是没有交过作业,哈哈。还好不会被叫家长。

Eleven老师的教学特点就是专心细致,所以印象中每一个班教学周期都挺长的,有时候真的担心周期太长,收入会不会低,不过这样的老师才能吸引更多小伙伴的注意吧。

但是,比较不建议没有基础的小伙伴学习,印象中第一个讲到的知识点就是泛型,当时也是咀嚼了很久才会运用。

建议

稀里糊涂写了那么多,培训机构的事情讲完,总归要总结一下,主要提这么几个建议,供大家参考:

  1. 想要学习一门技术,但是不知从何入手的人,可以从培训课程学起。
  2. 培训课程未必要花钱,网上有很多公开的免费资源,特别是编程类。
  3. 学习主要还是靠自学,老师能带你入门,传授给你学习方法,然后就要看自己了。
  4. 学习还要靠兴趣,实在学不下去的时候,就不要死磕了,循序渐进才能体会学习的乐趣,实在没有兴趣要及时“放弃”。
  5. 开发人员,要学会使用各种工具,要有自己解决问题的能力,如果遇事就找人问“为什么”,很难成长。要擅用各种技术文档/搜索引擎/工具书等。
  6. 关注几个自己专业相关的公众号,无论是行业的还是技术的。
  7. 要擅于从网上找轮子,不是所有的代码都要我们自己敲,但是轮子里的代码能看懂尽量还是要多看一看。
  8. 记笔记,学习的时候要强迫自己做笔记,无论是书面的还是电子文档。
  9. 自学能力比较好的,就不建议报名培训班了,专业知识还是专业的工具书说的更明白,买两本纸质书或Kindle上买两本电子书,学习的更快更透彻。

先说这么多吧,以后想起来了再补,另外后面会更新一些在软谋教育的学习笔记,整理以后发上来,基础学习的时候都是手写早就丢了所以没法整理,后面学习都是用电子档,还好处理一些。

LIS通讯常见的一些技术名词

前言

实验室信息系统仪器接口,从名称上来看,就可以很明确的知道这是一个负责做数据交互的工具。

但是对于刚入门的人来说,面对很多专业的名词,这其实并不简单。

虽然负责接口开发两年了,但是我也不敢说以我粗陋的了解,介绍的一定准确,这里就抛砖引玉,介绍一些专业名词和知识,如有错漏请指点一二。

HIS与LIS

这个词既然在看这篇文章,应该都不会陌生,这里摘录一下百度百科对于HISLIS的介绍,如果不了解的话可以参考一下:

医院管理和医疗活动中进行信息管理和联机操作的计算机应用系统,英文缩写HIS。HIS是覆盖医院所有业务和业务全过程的信息管理系统。按照学术界公认的MorrisF.Collen所给的定义,应该是:利用电子计算机和通讯设备,为医院所属各部门提供病人诊疗信息 ( Patient Care Information) 和行政管理信息(Administration Information)的收集(Collect)、存储(Store)、处理(Process) 、提取(Retrieve)和数据交换(Communicate) 的能力并满足授权用户 ( Authorized Users)的功能需求的平台。

LIS全称Laboratory Information Management System,是专为医院检验科设计的一套实验室信息管理系统,能将实验仪器与计算机组成网络,使病人样品登录、实验数据存取、报告审核、打印分发,实验数据统计分析等繁杂的操作过程实现了智能化、自动化和规范化管理。有助于提高实验室的整体管理水平,减少漏洞,提高检验质量。

通信模式

手工

在没有检验信息系统(LIS)时,检验医师需要给病人出具书面的检验报告,一般都是将实验器材出具的结果,或者检验医师观察到的结果,抄录到纸上,或者人工录入到Excel、Word文档再打印,最后提供给病人和医生,作为诊断和治疗的依据,这就是手工。

当然有一些实验仪器,自带了简单的报告系统,可以直接出具检验报告,这个我们另说不在讨论范围之内。

单工

手工出具结果,就存在效率低、易错等问题,而且不方便实验数据统一管理。

而绝大多数实验仪器都提供了通信方案,可以将实验仪器内的数据发送到LIS,或者提供了固定的存储位置供LIS采集。

这种仪器向LIS传输结果,或者LIS主动采集实验结果,因为数据的提供是单向的,固定由仪器提供给LIS,所以称之为单工,开发出的数据接收或采集接口称之为单工接口。

双工

单工接口解决了抄录实验结果效率低、易错的问题,但是对于每天大量的标本与繁多的检验项目,在上机做实验时,检验医师仍可能出错,例如将不同病人的标本放置错误而导致检验结果混淆,或漏选、多选、错选检验项目浪费试剂或错失最佳的检验时间。

由于如今信息化建设的逐步完善,HIS与LIS间已经没有隔阂,LIS可以很轻易的访问到医生为病人开具的检验医嘱信息,而医嘱信息是可以与检验项目挂钩,所以LIS系统可以将检验项目直接提供给检验仪器。

一般这个过程是标本采集后粘贴条码,检验科扫描条码签收,同时从HIS获取到标本的检验医嘱信息。

LIS接口从LIS中,查询签收条码的检验信息,将条码绑定的检查项目分别发送到指定的检验仪器。

医生直接将标本放置到对应仪器上,检验自动开始。

仪器会扫描标本上的条码,匹配LIS接口发送到检验仪器的项目直接进行实验。中间无需检验医师操作仪器选择项目。

(如果没有匹配到,检验仪器一般会发送查询消息给LIS系统,LIS接口接收到查询请求也可以返回检查项目。)

实验完成后,仪器自动将结果推送到LIS。

这个数据双向通信的过程我们称之为双工,而具备这种功能的LIS接口我们称之为双工接口。

流水线

流水线其实已经应用的很多,但是我实际接触到的不过一两家,因为目前来说大多数还不是特别完善,流水线需要精心保养否则容易出现问题。

但是流水线的应用前景是特别好的,也是实现检验自动化所必须的。

流水线就是完全不需要人工干预检验,检验医师只需要在监控整个系统的正常运行,并对于一些异常的检验结果进行处置。

检验科签收标本后,放置到流水线,流水线将自动将标本进行前处理包括离心、分样后,由传送带传送到对应仪器进行检验。

当然能供流水线正常运行的基础,是LIS系统必须实现了双工接口,能及时的将检验项目发送到流水线中,再由流水线将检验信息向各仪器分发,实现了检验过程的全自动化。

常见的通信方式

为了方便数据传输,一般仪器都提供了至少一种通信的方案,这里我们简单做一下介绍。

串口

串口通讯是最常见的通信方案,但是也较容易出现问题,其对于线材、传输距离、通信环境的要求都比较高,而且出现问题比较难排查,所以一般在数据校验上要认真对待避免出错。

当然处理过程中,如果确认了接口程序不存在问题,也要敢于对硬件提出质疑,包括线材和通讯环境等。

网口

网口大家都不会陌生,因为基本都应该学过或接触过Socket通信。

使用网口实现通信一般使用双网口电脑与实验仪器直连,或是将仪器接入路由器局域网通信,但就要求局域网网络绝对稳定,否则极易出现问题。

而一般此类通信协议常见的是使用TCP协议,一般是接口作为服务端,监听来自仪器的请求。也有极少数使用UDP协议HTTP协议

文件

就是直接将数据写入到计算机硬盘指定位置,供第三方读取检验结果,一般是普通文本或 *.csv 格式文件,也有输出为 *.xml 或Excel文件的。

数据库

这个大家也不陌生,一般仪器软件是将实验数据写入到数据库中间表,或者提供视图供第三方读取,常见的使用微软的SQL Server数据库和Access数据库。

USB

实际接触较少,一般是模拟串口进行数据通信,但是大部分由于驱动的问题极不稳定。

如果容易出现问题,建议联系仪器厂家沟通有没有其他通信方式,如果可以选择其他方式通信。

通讯协议

对于文件、数据库方式数据的交互基本比较简单,能够解读直接解析内容进行传输即可。

但是对于网口、串口这种通讯方式,没有一个固定的格式,很难区分怎样的数据是一份完整的结果。

而仪器厂家则会制定规则,并编写通讯协议,提供给LIS厂商参考,确定数据通信的格式,包括起始符,终止符,响应码,数据校验规则等。

这其中最常见的底层的通讯协议,也是国际上比较认可的通讯协议是 ASTM 协议与 HL7 协议,ASTM协议多见于串口通信,而HL7协议多见于网口通信。

当然也有一些厂商会在网口通信的情况下使用ASTM协议,这些也只是保证数据传输正确的一些通讯方案,不必诧异。

年龄的计算问题 C# 篇

前言

程序中经常会遇到需要计算具体年龄的问题,所以花费了一些时间思考了解决方案,根据网上已有的计算方式,做了简单封装与测试调优,最后封装了一个Age类完成年龄的计算。

单位问题

目前在项目中接触到的年龄单位分别是 岁、月、周、天、时、分,但是由于有些系统要求,年龄需要尽量具体,所以这里提供了一个标志枚举。至于具体什么是标志枚举,这里还没有特别做过介绍,所以大家可以搜索枚举Flags特性了解。以后有机会介绍Logger帮助类的时候再具体展开为大家介绍。

AgeUnit 枚举代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// <summary>
/// 年龄单位
/// </summary>
[Flags]
public enum AgeUnit
{
/// <summary>
///
/// </summary>
Year = 64,

/// <summary>
///
/// </summary>
Month = 32,

/// <summary>
///
/// </summary>
Week = 16,

/// <summary>
///
/// </summary>
Day = 8,

/// <summary>
///
/// </summary>
Hour = 4,

/// <summary>
///
/// </summary>
Minute = 2,

/// <summary>
///
/// </summary>
Second = 1
}

周岁年龄

年龄计算其实很简单,这个和我们小学时候做减法是一样的,首先从小单位着手运算,当相减为负数时向上一个单位借1

例如:当前时间是 2019-03-30 16:00:00,宝宝的出生日期是 2019-03-28 20:00:00,这个时候我们用 16时 - 20时 是不可行的,所以向日期借一天,就可以知道宝宝的实际年龄是 1天20时

这样看来是不是其实就是小学问题,但是有些人被年月日时分秒不同的进制搞得晕头转向,于是简单的问题被复杂化。

另外一点就是年月日之间的换算,进制是在变化的,但是这主要是由于每个月的天数不同,但是程序中已经提供了方法可以轻而易举的获取到某一年的某一个月的天数,所以这点我们也不用担心,下面我们看一下周岁年龄的计算:

Age 类型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/// <summary>
/// 周岁年龄
/// </summary>
public class Age
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="age">年龄的书面表达形式</param>
/// <param name="current">截止时间 用来计算出生日期</param>
public Age(string age, DateTime? current = null)
{
Current = current == null ? DateTime.Now : current.Value;
ParseAge(age);
}

/// <summary>
/// 构造函数
/// </summary>
/// <param name="birthday">生日</param>
/// <param name="current">截止时间 默认当前时间</param>
public Age(DateTime birthday, DateTime? current = null)
{
Birthday = birthday;
Current = current == null ? DateTime.Now : current.Value;
GetAge();
}

/// <summary>
/// 生日
/// </summary>
public DateTime Birthday { get; set; }

/// <summary>
/// 截止时间
/// </summary>
public DateTime Current { get; set; }

/// <summary>
/// 差值
/// </summary>
public TimeSpan TimeSpan { get; set; }

/// <summary>
/// 年龄单位
/// </summary>
public AgeUnit AgeUnit { get; set; }

/// <summary>
///
/// </summary>
public int Year { get; set; }

/// <summary>
///
/// </summary>
public int Month { get; set; }

/// <summary>
///
/// </summary>
public int Week { get; set; }

/// <summary>
///
/// </summary>
public int Day { get; set; }

/// <summary>
///
/// </summary>
public int Hour { get; set; }

/// <summary>
///
/// </summary>
public int Minute { get; set; }

/// <summary>
///
/// </summary>
public int Second { get; set; }

/// <summary>
/// 获取年龄指定单位的数值(非总年龄)
/// </summary>
/// <param name="unit">年龄单位</param>
/// <returns>该单位下的年龄</returns>
public virtual int GetAgeByAgeUnit(AgeUnit unit)
{
if (unit == AgeUnit.Year)
return Year;
else if (unit == AgeUnit.Month)
return Month;
else if (unit == AgeUnit.Week)
return Week;
else if (unit == AgeUnit.Day)
return Day;
else if (unit == AgeUnit.Hour)
return Hour;
else if (unit == AgeUnit.Minute)
return Minute;
else if (unit == AgeUnit.Second)
return Second;
else
throw new ArgumentOutOfRangeException(nameof(unit));
}

/// <summary>
/// 年龄单位
/// </summary>
protected virtual Dictionary<AgeUnit, string> AgeUnits { get; }
= new Dictionary<AgeUnit, string>()
{
[AgeUnit.Year] = "岁",
[AgeUnit.Month] = "月",
[AgeUnit.Week] = "周",
[AgeUnit.Day] = "天",
[AgeUnit.Hour] = "时",
[AgeUnit.Minute] = "分",
[AgeUnit.Second] = "秒",
};

/// <summary>
/// 用于提取年龄信息的正则字段
/// </summary>
protected Regex _regex;
/// <summary>
/// 用于提取年龄信息的正则
/// </summary>
protected virtual Regex Regex
{
get
{
if (_regex == null)
{
_regex = new Regex($@"(\d+)\D*(({string.Join(")|(", AgeUnits.Values.ToArray())}))\D*");
}
return _regex;
}
}

/// <summary>
/// 重写ToString方法
/// </summary>
/// <returns>年龄</returns>
public override string ToString()
{
StringBuilder builder = new StringBuilder();

for (int i = (int)AgeUnit.Year; i > 0; i /= 2)
if (((AgeUnit)i & AgeUnit) != 0 && GetAgeByAgeUnit((AgeUnit)i) != 0)
builder.Append(GetAgeByAgeUnit((AgeUnit)i)).Append(AgeUnits[(AgeUnit)i]);

if (string.IsNullOrEmpty(builder.ToString()))
builder.Append(0).Append(AgeUnits[AgeUnit.Year]);

return builder.ToString();
}

/// <summary>
/// 计算详细的周岁年龄
/// </summary>
protected virtual void GetAge()
{
if (Birthday > Current)
{
throw new ArgumentException("出生日期不能大于当前时间!");
}

//出生时间信息
int bYear = Birthday.Year;
int bMonth = Birthday.Month;
int bDay = Birthday.Day;
int bHour = Birthday.Hour;
int bMinute = Birthday.Minute;
int bSecond = Birthday.Second;

//当前时间信息
int nYear = Current.Year;
int nMonth = Current.Month;
int nDay = Current.Day;
int nHour = Current.Hour;
int nMinute = Current.Minute;
int nSecond = Current.Second;

//若出生时间信息小于当前时间 调整其差值为正整数
if (nSecond < bSecond)
{
nMinute--;
nSecond += 60;
}
if (nMinute < bMinute)
{
nHour--;
nMinute += 60;
}
if (nHour < bHour)
{
nDay--;
nHour += 24;
}
if (nDay < bDay)
{
nMonth--;
nDay += DateTime.DaysInMonth(bYear, bMonth);
}
if (nMonth < bMonth)
{
nYear--;
nMonth += 12;
}

//属性赋值
Year = nYear - bYear;
Month = nMonth - bMonth;
Day = nDay - bDay;
Hour = nHour - bHour;
Minute = nMinute - bMinute;
Second = nSecond - bSecond;
TimeSpan = Current - Birthday;
if (Year > 0)
AgeUnit = AgeUnit | AgeUnit.Year;
if (Month > 0)
AgeUnit = AgeUnit | AgeUnit.Month;
if (Week > 0)
AgeUnit = AgeUnit | AgeUnit.Week;
if (Day > 0)
AgeUnit = AgeUnit | AgeUnit.Day;
if (Hour > 0)
AgeUnit = AgeUnit | AgeUnit.Hour;
if (Minute > 0)
AgeUnit = AgeUnit | AgeUnit.Minute;
if (Second > 0)
AgeUnit = AgeUnit | AgeUnit.Second;
}

/// <summary>
/// 年龄转换
/// </summary>
protected virtual void ParseAge(string input)
{
Match match = Regex.Match(input);
MatchCollection collection = Regex.Matches(input);

if (collection.Count <= 0)
throw new ArgumentException(nameof(input));
else
{
foreach (Match item in collection)
{
if (int.TryParse(item.Groups[1].Value, out int age))
{
AgeUnit unit = AgeUnits.FirstOrDefault(kv => kv.Value == item.Groups[2].Value).Key;
SetAge(unit, age);
}
else
throw new ArgumentException(nameof(input));
}
}

GetBirthdayByAge();
TimeSpan = Current - Birthday;
}

/// <summary>
/// 通过年龄信息获取出生日期信息
/// </summary>
protected virtual void GetBirthdayByAge()
{
Birthday = Current.AddYears(-Year).AddMonths(-Month).AddDays(-Week * 7).AddDays(-Day).AddHours(-Hour).AddMinutes(-Minute).AddSeconds(-Second);
}

/// <summary>
/// 设置年龄信息
/// </summary>
/// <param name="unit">年龄单位</param>
/// <param name="age">年龄</param>
protected virtual void SetAge(AgeUnit unit, int age)
{
if (age == 0)
return;
AgeUnit = AgeUnit | unit;
if (unit == AgeUnit.Year)
Year += age;
else if (unit == AgeUnit.Month)
Month += age;
else if (unit == AgeUnit.Week)
Week += age;
else if (unit == AgeUnit.Day)
Day += age;
else if (unit == AgeUnit.Hour)
Hour += age;
else if (unit == AgeUnit.Minute)
Minute += age;
else if (unit == AgeUnit.Second)
Second += age;
else
throw new ArgumentException(nameof(unit));
}
}

以上主要需要看一下 GetAge 方法,通过 DateTime.DaysInMonth(year, month) 获取到出生月份的总天数,后面的运算就水到渠成了。

医学年龄

使用Age类进行计算虽然可以准确的计算一个人的周岁年龄,但是碰到一些极端情况,仍然存在问题。

例如一个人的年龄是 1月30天,这是由于出生月份的天数是31天导致的;再比如一个人的年龄是1月,但实际上只有29天或者28天,这是由于出生月份在2月份

这样可能会在一些对年龄计算有要求的系统中比如医疗机构的系统,引起用户的误解。当然我们可以与客户沟通,了解对方需求后对Age类型继承,然后提供出满足用户需求的计算年龄的解决方案。

例如以下是HIS提供商与医院沟通后确定的:固定1岁=365天1月=30天进行年龄计算的方案:

MedicalAge 类型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/// <summary>
/// 医学年龄
/// </summary>
public class MedicalAge : Age
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="age">年龄的书面表达形式</param>
/// <param name="current">截止时间 用来计算出生日期</param>
public MedicalAge(string age, DateTime? current = null) : base(age, current)
{

}

/// <summary>
/// 构造函数
/// </summary>
/// <param name="birthday">生日</param>
/// <param name="current">截止时间 默认当前时间</param>
public MedicalAge(DateTime birthday, DateTime? current = null) : base(birthday, current)
{

}

/// <summary>
/// 年龄1
/// </summary>
public int? Age1 { get; set; }

/// <summary>
/// 年龄2
/// </summary>
public int? Age2 { get; set; }

/// <summary>
/// 年龄单位1
/// </summary>
public AgeUnit? AgeUnit1 { get; set; }

/// <summary>
/// 年龄单位2
/// </summary>
public AgeUnit? AgeUnit2 { get; set; }

/// <summary>
/// 重写ToString方法
/// </summary>
/// <returns>年龄</returns>
public override string ToString()
{
StringBuilder builder = new StringBuilder();
if (Age1 != null)
builder.Append(Age1.Value).Append(AgeUnits[AgeUnit1.Value]);
if (Age2 != null)
builder.Append(Age2.Value).Append(AgeUnits[AgeUnit2.Value]);

if (string.IsNullOrEmpty(builder.ToString()))
builder.Append(0).Append(AgeUnits[AgeUnit.Year]);

return builder.ToString();
}

/// <summary>
/// 计算医学年龄
/// </summary>
protected override void GetAge()
{
if (Birthday > Current)
{
throw new ArgumentException("出生日期不能大于当前时间!");
}

//属性赋值
TimeSpan = Current - Birthday;
Year = TimeSpan.Days / 365;
Month = TimeSpan.Days % 365 / 30;
Week = TimeSpan.Days % 365 % 30 / 7;
Day = TimeSpan.Days % 365 % 30 % 7;
Hour = TimeSpan.Hours;
Minute = TimeSpan.Minutes;
Second = TimeSpan.Seconds;
if (Year > 0)
AgeUnit = AgeUnit | AgeUnit.Year;
if (Month > 0)
AgeUnit = AgeUnit | AgeUnit.Month;
if (Week > 0)
AgeUnit = AgeUnit | AgeUnit.Week;
if (Day > 0)
AgeUnit = AgeUnit | AgeUnit.Day;
if (Hour > 0)
AgeUnit = AgeUnit | AgeUnit.Hour;
if (Minute > 0)
AgeUnit = AgeUnit | AgeUnit.Minute;
if (Second > 0)
AgeUnit = AgeUnit | AgeUnit.Second;

for (int i = (int)AgeUnit.Year; i > 0; i /= 2)
{
if (((AgeUnit)i & AgeUnit) != 0)
{
if (AgeUnit1 == null)
{
AgeUnit1 = (AgeUnit)i;
Age1 = GetAgeByAgeUnit(AgeUnit1.Value);
}
else if (AgeUnit2 == null)
{
AgeUnit2 = (AgeUnit)i;
Age2 = GetAgeByAgeUnit(AgeUnit2.Value);
}
else
break;
}
}
}

/// <summary>
/// 通过年龄信息获取出生日期信息
/// </summary>
protected override void GetBirthdayByAge()
{
Birthday = Current.AddDays(-Year * 365 - Month * 30 - Week * 7 - Day).AddHours(-Hour).AddMinutes(-Minute).AddSeconds(-Second);
GetMedicalAge();
}

/// <summary>
/// 获取年龄1与年龄2
/// </summary>
protected virtual void GetMedicalAge()
{
for (int i = (int)AgeUnit.Year; i > 0; i /= 2)
{
if (((AgeUnit)i & AgeUnit) != 0)
{
if (AgeUnit1 == null)
{
AgeUnit1 = (AgeUnit)i;
Age1 = GetAgeByAgeUnit(AgeUnit1.Value);
}
else if (AgeUnit2 == null)
{
AgeUnit2 = (AgeUnit)i;
Age2 = GetAgeByAgeUnit(AgeUnit2.Value);
}
else
break;
}
}
}
}

测试

除提供了根据出生日期计算年龄以外,上文还简单提供了一个根据年龄获取出生日期的方案,可以参看上文AgeMedicalAge第一个入参为字符串类型的构造函数进行了解。

至于年龄输出的话,暂时重写了ToString方法,进行格式化的输出,以下是简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void Main()
{
TestGetAge(new DateTime(1931, 09, 18));
TestGetAge(new DateTime(1931, 09, 18, 23, 59, 59));
TestGetAge(new DateTime(1949, 10, 01));
TestGetAge(DateTime.Now.Date.AddDays(-50));
TestGetAge(DateTime.Now);
Console.ReadKey();
}

public static void TestGetAge(DateTime birthday)
{
Age age = new Age(birthday);
Console.WriteLine($"出生时间为:{age.Birthday.ToString("yyyy-MM-dd HH:mm:ss")} 的 年龄为:{age.ToString()}");
Age tempAge = new Age(age.ToString());
Console.WriteLine($"出生时间为:{tempAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss")} 的 年龄为:{tempAge.ToString()}");
Age medicalAge = new MedicalAge(birthday);
Console.WriteLine($"出生时间为:{medicalAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss")} 的 年龄为:{medicalAge.ToString()}");
Age tempMedicalAge = new MedicalAge($"{medicalAge.Year}{medicalAge.Month}{medicalAge.Week}{medicalAge.Day}{medicalAge.Hour}{medicalAge.Minute}{medicalAge.Second}秒");
Console.WriteLine($"出生时间为:{tempMedicalAge.Birthday.ToString("yyyy-MM-dd HH:mm:ss")} 的 年龄为:{tempMedicalAge.ToString()}");
}

总结

年龄计算从上文来看其实从技术上来讲并不困难,但是主要的问题在于,要与用户进行沟通达成共识。

否则从日常理解来说,特别是小单位需要细致到月周天的年龄的计算,很容易让人误解。