C#代码规范
转自《编写高质量代码改善C#程序的157个建议》陆敏技
TDenSan
这个作者很懒,什么都没留下…
展开
-
80.用Task代替ThreadPool
你也许会奇怪,我们的任务是通过Cancel的方式处理的,为什么完成的状态IsCanceled那一栏还是False。在任务结束求值的方法TaskEndedByCatch中,如果任务是通过ThrowIfCancellation Requested方法结束的,对任务求结果值将会抛出异常OperationCanceledException,而不是得到抛出异常前的结果值。ContinueWith方法可以在一个任务完成的时候发起一个新任务,这种方式天然就支持了任务的完成通知:我们可以在新任务中获取原任务的结果值。原创 2023-02-07 13:39:45 · 76 阅读 · 0 评论 -
93.构造方法应初始化主要属性和字段
类型的其他引用类型字段也应该在构造器中初始化,比如specialB,因为需要保证类型的其他地方用到该字段的时候不会因为它是null而产生混淆。在构造方法中,必须首先为CEO赋值。因为只要存在公司实体,那么它首先就会有一个CEO。类型的属性应该在构造方法调用完毕前完成初始化工作。如果字段没有在初始化器中设置初始值,那么它就应该在构造方法中初始化。类型一旦被实例化,那么它就应该被视为具有完整的行为和属性。上面演示的是一个字段初始化。也就是说,可以将初始化器理解为构造方法的一部分。,它在经编译后,在构造方法的。原创 2023-02-07 10:20:49 · 85 阅读 · 0 评论 -
98.用params减少重复参数
如果方法的参数数目不定,且参数类型一致,则可以使用。关键字减少重复参数声明。原创 2023-01-30 16:45:13 · 57 阅读 · 0 评论 -
101.使用扩展方法,向现有类型“添加”方法
我们也许会考虑修改设计,直接修改sealed类型,然后为其发布一个新的版本,但这依赖于你拥有全部的源码。值得注意的一点是,扩展方法还能够扩展接口。它相当于让继承自IEnumerable接口的任何子类都拥有了Select方法,而这些Select方法在调用者看来,就好像是IEnumerable接口所声明的一样。但是我们知道,可以有更优美的形式让调用者像调用Student类型的实例方法一样来调用GetSexString了。(3)扩展方法的第一个参数必须是要扩展的类型,而且必须加上。原创 2023-01-30 15:58:36 · 58 阅读 · 0 评论 -
103.区分组合和继承的应用场合
到目前为止,似乎一直在说继承的优点。一个类,如果其继承体系达到3层(当然,凡事都有例外,WPF体系中的控件集成体系,以Shape为例,多达7层),就可以考虑停止了。随着项目的发展,组合的优势会逐渐体现出来,它良好的封装性使类型可以对外宣布:我只做一件事。从设计的角度来看,继承代表的是“Is a”,组合代表的是“Has a”。如果组合太多的类型,就意味着当前的类很可能做了太多的事情,它就需要拆分成两个类了。继承不具有这样的特性,在C#中,子类只能有一个基类(接口则放开这种限制,子类可以继承自多个接口)。原创 2023-01-30 15:18:30 · 264 阅读 · 0 评论 -
106.为静态类添加静态构造函数
比较理想的做法是,在类型SampleClass的内部对fileStream进行初始化。在上面的代码中,如果类型初始化不成功,会在类型的内部处理完毕,并不会将异常抛给调用者。因为有时候调用者甚至都不知道类型需要初始化什么内容,所以将初始化失败的异常处理交给上层是不合理的。使用静态构造方法的好处是,可以初始化静态成员并捕获在这过程中发生的异常。静态类可以拥有构造方法,这就是静态构造方法。(2)代码无法调用它,不像实例构造方法使用new关键字就可以被执行。对静态引用类型的初始化应该使用静态构造方法。原创 2023-01-30 14:51:47 · 355 阅读 · 0 评论 -
111.避免双向耦合
双向耦合在同一项目下,不会存在太多的问题,带来的只是设计问题。不过,如果两个类在不同的项目中时,就必须考虑解耦了,因为.NET不允许项目之间相互引用。一般来说,类型之间不应该存在双向耦合,如果有此类情况出现,则应该考虑重构。在类型B中,我们针对接口编程,也就是说,在B中的字段a不再是A类型,而是将其修改为ISample类型。如果A、B类型分别在两个项目中,则提炼出来的接口要放在新起的项目中,然后让A、B所在的两个项目分别引用这个接口所在的项目。在实际的编码中,可以考虑使用这些框架设计我们的项目。原创 2023-01-29 17:31:02 · 62 阅读 · 0 评论 -
126.用名词和名词组给类型命名
类型对应着现实世界中的实际对象。对象在语言中意味着它是一个名词。所以,类型也应该以名词或名词词组去命名。类型定义了属性和行为。虽然它包含行为,但不是行为本身。动词类的命名更像是类型内部的一个行为,而不是类型本身。原创 2023-01-29 17:20:38 · 54 阅读 · 0 评论 -
128.考虑让派生类的名字以基类名字作为后缀
Exception及其子类就是这样一个典型的例子。所有的异常都应该继承自System.Exception。派生类的名字可以考虑以基类名字作为后缀。这带来的好处是,从类型的名字上我们就知道它包含在哪一个继承体系中。在FCL中,这类常用的例子还有Attribute、EventArgs等。从这里我们可以看出,微软支持让派生类的名字以基类名字作为后缀。原创 2023-01-29 17:14:06 · 51 阅读 · 0 评论 -
129.泛型类型参数要以T作为前缀
我们在使用SampleMethod方法的时候,如果将类型的泛型由T改为Person,很容易在类型内部会不自觉人为Person是一个类型,而不是一个泛型。而SampleMethod2带来的困扰就会少一些,因为泛型在使用它的地方被声明了。当然,无论如何,我们都不应该为泛型指定一个模棱两可的命名。记住,只要是泛型,就应该以T作为前缀命名。当然,这仅仅是一种习惯,若果使用第二种命名方式,编译器并不会报错,但是作为调用者,也许不能意识到这里是一个泛型类型参数。作为一种约定,泛型类型的参数要以T作为前缀。原创 2023-01-29 17:08:06 · 221 阅读 · 0 评论 -
133&131.用camelCasing命名私有字段和局部变量,用PascalCasing命名公开元素
我们可以看到,所有私有字段,包括方法的参数及局部变量全部遵循首字母小写的cameCasing规则。一旦脱离了这种规则,在编码过程中很容易给自己造成混淆。私有变量和局部变量只对本类型负责,它们在命名方式也采用和开放的属性及字段不同的方法。之所以要采用这两种不同的命名规则,是为了便于开发者自己快速地区分它们。camelCasing和PascalCasing的区别是它的。我们首先会怀疑name是什么类型,其次也会怀疑其可访问性。原创 2023-01-29 16:52:06 · 156 阅读 · 0 评论 -
134.有条件地使用前缀
各类设计规范也总建议我们保持一个娇小的类型,但是往往事与愿违,大类型常常存在。在这种类型中,如果不使用前缀,我们很难区分一个类型是实例变量还是静态变量,或者是一个const变量。在这个例子中,我们知道,即使类型本身不是很长,但是存在方法参数和类型实例变量重名的情况下,为实例变量或者静态变量使用前缀也是必要的。注意,有时候,如果类型只有实例变量或者只有静态变量,我们也直接使用前缀,以区别该变量不是一个局部变量。最典型的前缀是m_,这种命名一方面是考虑到历史沿革中的习惯问题,另一方面也许我们确实有必要这么做。原创 2023-01-28 11:22:47 · 49 阅读 · 0 评论 -
135.考虑使用肯定性的短语命名布尔属性
肯定性形容词或者短语虽然表达了一个肯定的含义,但是这些单词或者短语现在都被用于命名事件或者委托,所以不应该用于布尔属性。布尔值无非就是True和False,所以应该用肯定性的短语来表示它,例如,以Is、Can、Has作为前缀。原创 2023-01-28 10:42:30 · 60 阅读 · 0 评论 -
137.委托和事件类型应添加上级后缀
如果用传统方式,我们可能看不出来这些类型是有基类的,但是委托和事件的关键字delegate和event已经指明了后面类型的基类是Delegate。委托按照委托类型的作用又单纯分为Delegate结尾和CallBack结尾,我们在声明委托的时候一定要注意区分这一点。委托类型本身是一个类,考虑让派生类的名字以基类名字作为后缀。事件类型是一类特殊的委托,所以事件类型也遵循本建议。,则使用CallBack结尾。原创 2023-01-28 10:34:58 · 48 阅读 · 0 评论 -
150.使用匿名方法、Lambda表达式代替方法
上面的代码中,SampleMethod方法需要完成的功能是查看list中有没有长度等于5的元素。Predicate是一个委托,它接收元素值,并返回元素是否符合要求这一结果。而真正工作的代码只有1行。引领的语句就是一个匿名方法。其次,匿名方法经过编译器编译之后,就和普通方法没有任何区别了。匿名方法带来的只是简化程序员的部分工作而已。方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐。更好的简化方法就是Lambda表达式。”连接(读作“goes to”),符号。左边是参数列表,右边是方法体。原创 2023-01-28 09:50:25 · 58 阅读 · 0 评论 -
71.区分异步和多线程应用场景
是的,上面的程序解决了界面阻滞的问题,但是,它高效吗?当开始I/O操作的时候,异步会将工作线程还给线程池,这时候就相当于获取网页的这个工作不会再占用任何CPU资源了。直到异步完成,即获取网页完毕,异步才会通过回调的方式通知线程池,让CLR响应异步完毕。可见,异步模式借助于线程池,极大地节约了CPU的资源。可以预见,如果该网页的内容很多,或者当前的网络状况不太好,获取网页的过程会持续较长时间。为了获取网页,CLR新起了一个工作线程,然后在读取网页的整个过程中,该工作线程始终被阻滞,直到获取网页完毕为止。原创 2022-12-20 17:42:53 · 146 阅读 · 0 评论 -
58.用抛出异常代替返回错误代码
在catch (CommunicationException)代码块中,代码所完成的功能是“通知发送”,而不是“发送”本身,因为我们需要确保在catch和finally中所执行的代码是可以被执行的。不应该将异常机制用于正常控制流中,异常的发生是一个小概率事件,所以异常带来的效率问题会被限制在一个很小的范围内。在本例中的catch代码块中,不是要真的编写发送邮件的代码,因为发送邮件的这个行为可能会产生更多的异常,而“通知发送”这个行为稳定性更高(即“不出错”)。但是,现在有了另一种选择,既使用异常机制。原创 2022-12-20 15:38:15 · 227 阅读 · 0 评论 -
53.必要时应将不再使用的对象引用赋值为null
当检测到方法内的“根”时,如果发现没有任何一个地方引用了局部变量,则不管是否已经显式将其赋值为null,都意味着该“根”已经被停止。然后,垃圾回收器会发现该根的引用为空,同时标记该根可被释放,这也代表着类型对象所占用的内存空间可以被释放。但是,在另一种情况下,却要注意及时地将变量赋值为null,那就是类型的静态字段。在CLR托管的应用程序中,存在一个“根”的概念,类型的静态字段、方法参数、以及局部变量都可以作为“根”的存在(值类型不能作为“根”,只有引用类型的指针才能作为“根”)。中c1 = null;原创 2022-12-20 15:08:35 · 139 阅读 · 0 评论 -
40.使用event关键字为委托施加保护
这应该是不允许的,因为什么时候通知调用者,应该是FileUploader类自己的职责,而不是调用者本身来决定的。event关键字正是在这种情况下被提出来的,它为委托加了保护。事件“MyTest.FileUploader.FileUploaded”只能出现在 += 或 -= 的左边(在类型“MyTest.FileUploader”中使用时除外)以上代码将编译不通过。原创 2022-12-15 15:25:22 · 55 阅读 · 0 评论 -
38.小心闭包中的陷阱
所谓闭包对象,指的是上面这种情形中的TempClass对象(在第一段代码中,就是编译器为我们生成的c__DisplayClass2对象)。如果匿名方法(lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到闭包对象中,即将for循环中的变量i修改成了引用闭包对象的公共变量i。这样,即使代码执行离开了原局部变量i 的作用域(如for循环),包含该闭包对象的作用域还存在。这段代码并不像我们想象的那么简单,要完全理解运行时代码是怎么运行的,首先必须理解C#编译器为我们做了什么。原创 2022-12-15 14:54:11 · 126 阅读 · 0 评论 -
37.使用Lambda表达式代替方法和匿名方法
使用匿名方法后,我们就不需要再Main方法外部声明两个方法了,可以直接在Main这个工作方法中完成所有代码编写,而不会影响代码清晰性。,其本质是匿名方法。实际上,经过编译后的Lambda表达式就是一个匿名方法。我们应该在实际编码中熟练运用它,避免出现繁琐且不美观的代码。实际上要完成相同的功能,还有很多种编码方式。注意:上面的语法虽然繁琐,但是我们可以从中加深对委托本质的认识:委托也是一种。Lambda表达式操作符“=>”的左侧是。,跟任何FCL中的引用类型没有差别。),我们都建议采用这种方式来编写。原创 2022-12-15 10:13:55 · 241 阅读 · 0 评论 -
36.使用FCL中的委托声明
在FCL中每一类委托声明都代表一类特殊的用途,虽然可以使用自己的委托声明来代替,但是这样做不仅没有必要,而且会让代码失去简洁性和标准性。在我们实现自己的委托声明前,应该首先查看MSDN,确信有必要后才这样做。FCL中存在3类这样的委托声明,它们分别是:Action、Func、Predicate。注意:很少方法的参数能够超过16个,如果真有这样的参数,首先要考虑自己设计是否存在问题。Action的重载版本有17个,最多参数的重载有16个参数。Func的重载版本有17个,最多参数的重载有16个参数。原创 2022-12-15 09:33:38 · 62 阅读 · 0 评论 -
34.为泛型参数设定约束
但是,在添加了约束后,我们会发现参数t1和t2变成了一个有用的对象。“约束”这个词可能会引起歧义,有些人可能认为对泛型参数设定约束是限制参数的使用,实际情况正好相反。没有“约束”的泛型参数作用很有限,倒是“约束”让泛型参数具有了更多的行为和属性。在编程过程中应该考虑为泛型参数设定约束,约束使泛型参数成为一个实实在在的“对象”,让它具有了我们想要的行为和属性,而不仅仅是一个object。(7)可以对同一类型的参数应用多个约束,并且约束自身可以是泛型类型。(4)指定参数必须是指定的基类,或者派生自指定的基类。原创 2022-12-13 17:36:55 · 70 阅读 · 0 评论 -
104.用多态代替条件语句
随着DriveCommand元素的增加,采用if或switch语句将带来可怕的混乱状态是显而易见的。在一个复杂的控制系统中,命令可能会多达上百条。原来的设计理念也是欠妥当的,它不遵守设计模式中的“开闭原则”。开闭原则是指:对扩展开发,对修改关闭。遵从开闭原则的一次重构是,使用多态来规避不断膨胀的条件语句。可见,代码简洁了不少,而且,可扩展性增强了。即使未来还需要增加命令,扩展相应的子类就可以了。而且我们关闭了修改,即对Drive方法,即使增加再多的命令,也不需要对其进行修改。假设要开发一个自动驾驶系统。原创 2022-12-13 14:44:24 · 73 阅读 · 0 评论 -
149.使用表驱动法避免过长的if和switch分支
枚举元素代表的整型值,很容易和字符串数组索引结合起来,用两行语句就解决了GetChineseWeek方法。但是,这种方法有局限性,如果需要换成:星期一Mike打扫卫生、星期二Rose清理衣柜、星期三Mike和Rose没事可以吵吵架、星期四Rose要去Shopping,也就是说需求由静态属性变成了动态行为,那么事情就变得复杂了。当然,星期制已经是固定的了,应该不会出现扩展情况。如果增加条件分支,不必修改源代码,直接增加子类就可以了。利用多态避免分支,详见建议104,本建议要采用的是“表驱动法”。原创 2022-12-13 14:39:56 · 95 阅读 · 0 评论 -
21.选择正确的集合
在命名空间System.Collections.Concurrent下,还涉及几个多线程集合类:ConcurrentBag、ConcurrentDictionary、ConcurrentQueue、ConcurrentStack,分别对应List、Dictionary、Queue、Stack。队列Queue遵循的是先进先出模式,它在集合末尾添加元素,在集合的起始位置删除元素。,就是相互之间存在一种或多种特定关系的数据元素的集合。原创 2022-12-13 10:43:49 · 64 阅读 · 0 评论 -
1.正确操作字符串
当然,我们需要注意,StringBuilder指定的长度要合适,太小了,需要频繁分配内存;对CLR来说,string对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用System.String 类中的任何方法或进行任何运算(如“=”赋值、“+”拼接等),都会在内存中创建一个新的字符串对象,这也意味着要为该新对象分配新的内存空间。在使用其他值引用类型到字符串的转换并完成拼接时,应当避免使用操作符“+”来完成,而应该使用值引用类型提供的ToString方法。[会分配新的堆内存]原创 2022-12-13 09:04:27 · 41 阅读 · 0 评论 -
17.多数情况下使用foreach进行循环遍历
在MyList的内部,默认返回MyEnumerator,MyEnumerator就是迭代器的一个实现,如果对于迭代的需求有变化,可以重新开发一个迭代器(如下所示),然后在客户端迭代的时候使用该迭代器。而且,由于客户端代码过多地关注了集合内部的实现,代码的可移植性就会变得很差,这直接违反了面向对象的开闭原则。于是,迭代器模式就诞生了。在客户端的代码中,我们在迭代的过程中分别演示了for循环和while循环,到那时因为使用了迭代器的缘故,两个循环都没有针对MyList编码,而是实现了对迭代器的编码。原创 2022-12-12 15:50:34 · 127 阅读 · 0 评论 -
18.foreach不能代替for
foreach循环使用了迭代器进行集合的遍历,它在FCL提供的迭代器内部维护了一个对集合版本的控制。那么什么是集合版本?简单来说,其实它就是一个整形的变量,任何对集合的增删操作都会使版本号加1。上一个建议中提到了foreach的两个优点:语法更简单,默认调用Dispose方法,所有我们强烈建议在实际的代码编写中更多的使用foreach。如果使用for循环就不会带来这样的问题。for直接使用索引器,它不对集合版本号进行判断,所以不会存在以为集合的变动而带来的异常(当然,超出索引长度这种异常情况除外)。原创 2022-12-12 15:22:37 · 99 阅读 · 0 评论 -
15.使用dynamic来简化反射实现
var实际上 是编译期抛给我们的“语法糖”,一旦被编译,编译期会自动匹配var 变量的实际类型,并用实际类型来替换该变量的声明,这看上去就好像我们在编码的时候是用实际类型进行声明的。而dynamic被编译后,实际是一个 object类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期。我们可能会对这样的简化不以为然,毕竟代码看起来并没有减少多少,但是,如果考虑到效率兼优美两个特性,那么dynamic的优势就显现出来了。原创 2022-12-12 15:02:14 · 98 阅读 · 0 评论 -
13.为类型输出格式化字符串
实际上,在第一个版本的Person类型中,如果对IFormattable的ToString方法稍作修改,就能让格式化输出在语法上支持更多的调用方式。但即使是重写了ToString方法,提供的字符串输出也是非常单一的,而通过实现IFormattable接口的ToString方法, 可以让类型根据用户的输入而格式化输出。最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Object的ToString方法,它会。更多的时候,类型的使用者需为类型。返回当前类型的类型名称。原创 2022-12-12 14:41:29 · 51 阅读 · 0 评论 -
11.区别对待==和Equals
比如,对于string这样一个特殊的引用类型,微软觉 得它的现实意义更接近于值类型,所以,在FCL中,string的比较被重载为针对“类型的值”的比较,而不是针对“引用本身”的比较。由于操作符“==”和“Equals”方法从语法实现上来说,都可以被重载为表示“值相等性”和“引用相等性”。如果比较的两个变量引用的是内存中的同一个对象,那么将其定义为“引用相等性”。一般来说,对于引用类型,我们要定义“值相等性”,应该仅仅去重载Equals方法,同时让“==”表示“引用相等性”。原创 2022-12-12 14:11:03 · 44 阅读 · 0 评论 -
8.避免给枚举类型的元素提供显式的值
当编译器发现元素ValueTemp的时候,它会自动在Tuesday = 2的基础上+1,所以,实际ValueTemp的值和Wednesday的值都是3。事实上,如果为枚举类型显式地赋过值,那么很有可能在下个版本中,你为了某些增加的需要,会为枚举添加元素,在这个时候,就像我们为Week增加元素ValueTemp一样,极有可能会一不小心增加一个无效值。不正确地为枚举类型的元素设定显式的值,会带来意想不到的错误。,这样一来,就要求枚举的每个元素的值都是 2 的若干次幂,指数依次递增。注意,本建议也有例外。原创 2022-12-12 11:59:00 · 52 阅读 · 0 评论 -
7.将0值作为枚举的默认值
Week看上去多了第8个值,同时,很不幸,这段代码没有引发异常。所以,应该始终为枚举的0值指定默认值。在上面的枚举类型Week中,可以将。允许使用的枚举类型有byte、sbyte、short、ushort、int、uint、long和ulong。不过,这样做不是因为允许使用的枚举类型在声明时的默认值是0,而是有工程上的意义。注意,除了上文说的Week的第8个值外,其实,如果枚举类型的元素类型为。去掉,编译器会自动从0值开始计数,然后逐个为元素的值+1。那么,编写了如下的代码,它的输出会是什么呢?原创 2022-12-12 11:32:24 · 190 阅读 · 1 评论 -
6.区别readonly和const的使用方法
readonly所代表的运行时含义有一个重要的作用,就是可以为每个类的实例指定一个readonly的变量。readonly的全部意义在于,它在运行时第一次被赋值后将不可以改变。而之所以说const变量的效率高,是因为经过编译器编译后,我们在代码中引用const变量的地方会用const变量所对应的实际值来代替,如。关于第一个区别,因为const是编译期常量,所以它天然就是static的,不能手动再为const增加一个static修饰符。(1)对于值类型变量,值本身不可改变(readonly,只读)。原创 2022-12-12 11:18:11 · 88 阅读 · 0 评论 -
5.使用int?来确保值类型也可以为null
可以为null的类型表示其基础值类型正常范围内的值再加上一个null值。例如,Nullable,其值的范围为-2147483648 ~ 2147483 647,再加上一个null值。在C#中,值被取出来后,为了将它赋值给int类型,不得不首先判断一下它是否为null。int j = i?表示的意思是,如果i的HasValue为true,则将i的value赋值给j;从.NET 2.0开始,FCL中提供了一个额外的类型:可以为空的类型Nullable。下面是可空类型和基元类型的互相转换。原创 2022-12-12 10:39:53 · 1257 阅读 · 0 评论 -
4.TryParse比Parse好
两者最大的区别是,如果字符串格式不满足转换的要求,Parse方法将会引发一个异常(引发异常这个过程会对性能造成损耗);TryParse方法则不会引发异常,它会返回false,同时将result置为0。不过,不建议为所有的类型都提供TryParse模式,只有在考虑到Parse方法会带来明显的性能损耗时,才建议使用TryParse。除string外的所有基元类型,它们都有两个将字符串转型为本身的方法:Parse和TryParse。原创 2022-12-12 10:20:45 · 238 阅读 · 0 评论 -
3.区别对待强制转型与as和is
强制转型可能意味着两件不同的事情:(1)FirstType和SecondType彼此依靠转换操作符来完成两个类型之间的转型。(2)FirstType是SecondType的基类。类型之间如果存在强制转型,那么它们之间的关系,要么是第一种,要么是第二种,不能同时既是继承的关系,又提供了转型符。第一种情况,代码如下:在这种情况下,如果想转型成功则必须使用强制转型,而不是使用as操作符。复杂一点的应用 :这段代码与上段代码相比,仅仅多了一层转型,实际上obj还是firstType,原创 2022-12-07 17:53:43 · 94 阅读 · 0 评论 -
2.使用默认转型方法
(1)类型的转换运算符转换运算符分为两类:隐式转换和显式转换(强制转换)。基元类型(编译器直接支持的数据类型,包括sbyte、byte、short、 ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string)普遍都提供了转换运算符。(2)使用类型内置的Parse、TryParse,或者如ToString、ToDouble、ToDateTime等方法(3)使用帮助类提供的方法可以使用如System.Convert类原创 2022-12-07 15:47:20 · 50 阅读 · 0 评论