分类:技术相关


分享编程等技术相关的心得

Effective C# 阅读笔记(二) New

为了提升一下自己的姿势水平,买了 和 两本书,想一边阅读一边强迫自己做点笔记(主要是自己的理解),不然怕是看了就忘。另外手头还有一本《深入解析C 最近比较烦心,没想到我也有成为雷霆崖老哥的一天,请大家一定要擦亮双眼,不要把真心花在烂人身上,一味容忍只会给你带来更大的痛苦。不过也好,谈恋爱哪有写代码爽,只是这半个月要回家过年,得等年后才能继续更新了。今天这篇是 的第二章。 简单来说.Net提供了垃圾回收器GC,帮助控制托管内存,让开发者不用担心内存泄漏等问题。GC在运行时会判断不在使用的对象为垃圾,将其回收,并压缩托管堆,把剩余的活动对象转移到连续的内存区域上。而非托管资源回收需要由开发者来管理控制。NET中提供释放非托管资源的方式主要是:finalizer(终结器)和接口。 finalizer在C 但这种方式适用于C 因此释放资源首选实现接口,然后主动调用方法来释放资源。 注:C 详细的接口实现将在第17条讲述。 如果都把字段的初始值设定放在构造函数里做的话,在构造函数多了之后可能就会忘记给部分字段设定初始值,所以不如在声明字段的时候就直接初始化。 但书中也给了三种不应该使用初始化语句的情况: 第一种:把对象初始化成0和null。 因为默认的初始化逻辑就是把对象初始化成0或null,自己手动再度设0和null没有意义。 第二种:构造函数都各自以不同方式初始化了字段。 不过我觉得书本这里逻辑也有点问题,本来前面就是说为了防止因构造函数太多导致漏掉字段赋初值,所以要在声明时初始化。结果这里又说如果构造函数都进行了初始化,那么就不要在声明的时候初始化了,那万一漏掉了怎么办,有点矛盾。还是老老实实、规规矩矩检查代码最重要。 第三种:初始化变量的时候可能抛出异常。 因为在声明的时候初始化是没有办法使用语句的,所以碰到这种情况应该把初始化过程放到构造函数里进行。 创建某类型的实例之前,应先初始化该类型的静态成员,这个工作交由静态构造函数进行(当然简单的初始化直接用初始化语句就可以了)。在初次访问这个类的方法、成员之前会执行静态构造函数。静态构造函数最常见的用途是实现单例模式(单例模式的、)。 所以给类中的静态成员设置初始值,简单的话直接用初始化语句,复杂的话,比如会抛出异常要做处理的,那么就使用静态构造函数。 有些时候需要实现各种输入参数的构造函数,里面有很大一部分逻辑是相同的,为了图方便有的人可能就直接把相同代码复制粘贴(书里还特别写了:笔者觉得你应该不是这种人吧)。正确的写法应该是把重复的逻辑放到一个共同的构造函数中,然后其他构造函数来调用该构造函数。这样写能减少重复代码,而且编译器也不会因为这种写法而反复调用基类的构造函数。下面是示例 看起来似乎跟把相同的初始化逻辑都放到一个单独的方法里,然后所有构造函数都调用这个方法的方式也没有什么不同?实际上这样做的效率要比链式调用构造函数低。因为这样的话,编译器无法合并构造函数中的相同操作,不得不在每个构造函数里都调用基类构造函数,然后执行这个共同用于初始化的方法。而且像变量只能通过初始化语句或者构造函数进行初始化,无法在其他方法中赋值,因此要么选择链式调用初始化,要么只能在不同构造函数里分别初始化。 另外,除了重载,也可以利用默认参数实现: 使用默认参数的写法较为简洁,也会让用户使用起来更方便。如果参数很多,那么用重载就需要编写大量不同版本的构造函数。因此比较推荐使用默认参数来实现构造函数。 但是有部分问题需要注意,首先是比如带有约束的泛型类或泛型方法需要的是真正的无参构造函数,如果只有所有参数都有默认值的构造函数也是不满足条件的,因为实际上实现的还是有参构造函数。所以如果开发者允许用户通过来调用构造函数,那么即使有所有参数都有默认值的构造函数,也应该实现一个真正的无参构造函数,让这种调用方法在所有场合都是可以使用的。 其次是方法的默认值作为编译时常量,有点类似第一章提到的const的行为,在编译时就会用实际值替换写死在程序中。如果调用的是外部的带有默认值参数的方法,某天被调用的该方法更新了默认值,但调用的程序没有重新编译,那么调用时仍然会使用旧的默认值,类似下面这样 不过在网上看到一个解决方法,用0或null作为默认行为的哨兵值。当被调用方法检测到参数为null或0时,就知道该使用默认参数,于是在方法中替换为真正的默认值。这样就能避免上面外部库中方法默认值更新带来的问题。 回顾一下C 之后再次构建该类型的其他实例,会直接从第5步开始执行。另外,可以通过链式调用构造函数来优化第6、7步。 总的来说,C 在堆上创建和销毁对象都是需要时间的,创建过多无谓的对象会大幅度降低性能。防止频繁创建局部对象有几个技巧。 第一个,如果在频繁调用的方法中需要创建同一个类型的对象,请考虑把它从局部变量改为成员变量,以实现复用。只有调用相当频繁的时候才值得这样做。 第二个,采用依赖注入。追求的是复用需要使用的对象,同时可以避免创建未使用的对象。不过我感觉这里有点问题,书上举的例子其实更类似单例模式? 依赖注入的话,我是在ASP.NET Core中接触到的,确实也能实现对象的复用,不过感觉更重要的作用还是解耦合。原理就是类型中的方法或成员需要依赖于类型的对象,现在改为依赖对应接口。这样类型的对象不需要控制该对象的生成,只需要在外部生成然后传入中使用即可。一旦后面替换成使用对象,直接给传入即可,从而实现了解耦合。 第三个技巧是针对不可变对象的,大家最熟悉的就是字符串类型,不应该用号频繁拼接字符串这个也是老生畅谈的问题了,因为会生成大量不需要的子字符串变成垃圾,增加GC压力。正确做法应该是,简单的用内插字符串,复杂的用。类似的,其他不可变类型也应该尽量使用对应的类来操作。 简单来说就是如果在构造函数里面调用虚函数逻辑很混乱,细节不一定能搞清楚,最终导致出现问题,书上举了个例子: 请问输出是什么? 所以答案是。可以看到非常绕且完全没有必要,因此绝对不要在构造函数里面调用虚函数。 第11条中讲到释放资源首选实现接口,实际上我们要实现的不止接口,而应该实现标准的dispose模式。 要实现dispose模式,书上给出了一些规则: 根部的基类需要做到: 子类应该做到: 接口中只包含了这一个方法,实现时需要注意四点: 另外前面说过和finalizer都应该把释放资源的工作交给虚方法完成,这样子类能够重写该方法来释放他们自己的资源。因此我们应当重载一个来完成实际的资源释放工作,让和finalizer都调用它。子类可以覆写这个虚方法,编写代码清理自身的资源并调用基类方法来清理基类资源。代表了是否是通过调用的,如果未则同时清理托管和非托管资源;如果为则表明是finalizer调用的,只需要清理非托管资源。如果是子类的话,还要调用基类的方法。 一个标准的dispose模式实现如下: 如果有子类继承该类的话,子类的实现为: 和finalizer最好只用来释放资源,如果加入其他操作的话请考虑清楚,防止导致本该已经宣告消亡的该对象重新被其他地方保留,因为理论上该对象已经被终结了,所以不会再被GC所清理,导致残留。 按照上面的模板编写标准的dispose模式,既方便了自己,也方便了用户和从该类中派生子类的开发者。因此应该让自己养成这个好习惯,在要实现dispose模式时尽可能按照标准模板去编写。 第二章完。

阅读全文

Effective C# 阅读笔记(一)

为了提升一下自己的姿势水平,买了 和 两本书,想一边阅读一边强迫自己做点笔记(主要是自己的理解),不然怕是看了就忘。另外手头还有一本《深入解析C 简单来说就是在声明局部变量时,应该使用而尽量避免使用类型的全称。的引入主要是为了匿名类型和Linq,跟动态类型不同,它是靠编译器自动推断出变量的实际类型。 这个还是比较好理解的,因为用很多时候方便且不会影响对代码的理解,常用的几个场景比如 但也不是所有局部变量都适合用,因为此时不好看出变量的类型,或编译器推断并不适合,比如 所以在保证能在阅读时清楚知道变量类型的情况下,应尽量使用做到简洁明了;反之就要明确指出类型,比如面对int,float,double等时。 是编译期常量,是运行期常量。区别在于编译时,所有用到常量的地方,该常量都会被常量实际值给完全替换,而用到常量的地方仍会保留对常量的引用,类似于 问题在于在库A中声明了一常量,若项目B引用了A的,编译B时也会自动将B中用到的自动替换为100。若后续库A将更新为200,但项目B没有重新编译,则B中仍为100。而改为常量则没有这个问题,因为使用的时候引用的时真实变量,而不是编译时就写死的值。 因此只有在对性能极端敏感和有需求将值在编译期就固定下来的时候才使用,不然最好选择。 运算符用来判断变量是否兼容(支持转换成)某种类型,运算符用来转换变量的类型。 因此我基本都是用第四种模式匹配的is语句来做类型转换,不过也有用的时候,主要结合(null条件运算符),比如 内插字符串也是C 可以看出内插字符串的可阅读性更好、更直观。且内插字符串功能丰富,在之间可以像原一样用格式说明符,也可以调用方法、使用null条件运算符、调用Linq等等。当然内插的表达式过于复杂就背离了我们使用内插字符串的初衷,不如先声明为局部变量再内插更直观。 也是通过上一条的内插字符串生成的,主要是用来实现全球化和本地化。编译器在推断内插字符串类型的时候会默认推断为string,如有使用需要需要手工指定为类型 然后需要编写一个方法用来将FormattableString转换为适应不同国家和地区的字符串,比如 某些地方会使用到变量的名字,比如WPF实现接口的时候,需要以字符串形式传入对应变量的名字实现绑定,如果直接用这种字符串常量写死会导致强耦合,一旦后来代码修改时更改了变量名字,那么就会出现问题且IDE没有提示。所以需要用到运算符,可以获取类型、变量、接口、命名空间的名字。 还有一点题外话: csharp int i = 123; // 装箱(隐式) object o = i; // 拆箱(显式) int j = (int)o; csharp // int类型和枚举类型的转换 public class VariantTypeToIntConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return (int)(VariantType)value; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return (VariantType)(int)value; } } csharp public class Parent { public void Print() { Console.WriteLine(nameof(Parent)); } } public class Child : Parent { // 不加new也不报错,但编译器会提醒若是有意隐藏父类成员请加new public new void Print() { Console.WriteLine(nameof(Parent)); } } csharp Child child = new Child(); Parent parent = new Child(); child.Print(); parent.Print(); // 输出 // Child // Parent csharp public class Parent { public virtual void Print() { Console.WriteLine(nameof(Parent)); } } public class Child : Parent { // override用来覆写虚方法 public override void Print() { Console.WriteLine(nameof(Child)); } } csharp Child child = new Child(); Parent parent = new Child(); child.Print(); parent.Print(); // 输出 // Child // Child ``` 书中建议,只有当你基于现有父类A的子类B包含方法X,而新版的A中添加了和B中同名的X时,可以让你的B中的X添加修饰符作为暂时的解决方案,当然就长远来看最好还是考虑改名。而不应该把当作的替代品用于覆写父类中非虚的方法。这也需要父类的设计者应该认真考虑清楚什么方法可能和应该交由子类重新实现,然后用修饰。 第一章完。

阅读全文

OpenCC.NET GUI——方便的中文转换工具

之前我基于OpenCC写过一个OpenCC.NET的库,写得也比较烂,就想稍微重构一下然后再写个即开即用的桌面端程序,于是就有了这个OpenCC.NET GUI。 本来是想尝鲜用Win UI3来实现的,最后还是决定稳妥点用了WPF 虽然我完全不懂设计,但程序总得有个Logo。于是我把「字」字做了一点改造,搞出一个类似转换箭头的结构,代表中文转换;然后分层加阴影和光照,尝试去模仿Fluent Design图标的那种质感,最后就得到了: 程序已经开源放在了GitHub和Gitee上,有兴趣的可以取看看,地址: 下面的部分我就偷懒直接照抄readme了。 OpenCC.NET GUI是基于OpenCC(Open Chinese Convert, 开放中文转换),使用实现的中文转换工具,支持中文简繁体之间词汇级别的转换,同时还支持地域间异体字以及词汇的转换。 可以直接在仓库页面右侧获取最新Release版本,或下载源码后自行编译。若无法运行,请确保安装了。 软件提供了文本编辑转换和文件批量转换两种功能。 此模式下直接在文本框中输入你所需要转换的语句,在右侧选择转换选项后,点击转换按钮即可得到结果。 此模式下可以批量导入txt文本文件进行转换。若生成文件为乱码,请确保原文件为UTF 实现文本转换。 为OpenCC.NET提供词库。 为OpenCC.NET提供分词。 提供UI控件。 提供选择文件夹窗口(没错,WPF没有自带选择文件夹窗口)。

阅读全文

基于RSA算法的软件授权生成和验证

又摸了好久,结果博客都好几个月没更新,是时候水一水了。刚好前段时间听说项目要加授权功能,便对如何实现这种利用激活码进行授权的形式产生了兴趣,决定动手试一试,并以此文加以记录。 我对加密算法相关的东西完全一窍不通,据我搜索的资料(指用百度和Google,如有错误请指出),适用于生成软件激活码这种场景的是非对称加密算法。 非对称加密算法和对称加密算法的区别主要在于密钥,对称加密算法只有一个密钥,如果用于激活码生成,那么在生成端和验证端都必须保存这个密钥,用户就能比较简单地获取。之后用户一旦得到一个可用激活码,就可以将其解码后按照授权信息结构伪造激活码。 而非对称加密算法则有一个公钥(较简单)和一个密钥(较复杂),公钥可以公开,而私钥则要保密。生成激活码时使用私钥,用户只能接触到公钥,而依靠公钥只能验证激活码但无法生成激活码,就达到了我们的目的。 非对称加密算法中一种常用算法是RSA算法,如果使用SSH时有生成和使用过SSH key的话应该会有印象。它的公钥(PEM格式)形式为: 私钥(PEM格式)形式为: RSA算法主要有两种用途,一种是加密与解密信息,另外一种是签名与验证信息。 RSA算法的加密与解密功能,主要是把原始信息利用公钥加密,传输给他人后,利用私钥将其解密重新得到原始信息。这主要保证了信息不会被泄露,因为公钥只能加密不能解密,即使被他人获取了也没有关系。而只有手中有私钥的人才能解密,获取原始信息的内容。 这里引用一段所举的例子: 那么能不能利用加密和解密来实现激活码生成呢?我个人认为算是勉强可以。虽然按照原理来说必须靠公钥加密,私钥解密,也就是公钥生成激活码,私钥对激活码解码。可上文说了私钥必须保密,怎么能保存在客户端呢?但我们其实也可以把两者的地位交换,即把原始公钥保密,作为密钥;而把原始密钥公开,作为公钥。这样就可以用新密钥(原公钥)加密授权信息制作激活码,然后在客户端保存新公钥(原密钥),用于解密和检查激活码。 但实际上RSA算法的密钥包含了公钥信息(所以密钥才会相对公钥更复杂),用这种方式制作的激活码只能防住一般用户,如果用户从新公钥(原密钥)中推导出了新密钥(原公钥),那么我们授权系统也基本等于被攻破了。 RSA算法的签名与验证功能,主要是把原始信息利用密钥对其进行签名,附上原始信息一起传输给他人后(签名只能供验证用途,无法恢复出原始信息,所以要附带上原始信息),利用公钥对签名和原始信息进行验证,若验证通过则说明信息真实有效。这主要保证了信息不会被篡改,因为只有手中有密钥的人才能制造有效签名来实现和篡改的信息一致,否则篡改信息无法通过公钥的验证。 这里再引用一段所举的例子: 因为加密与解密方案行不通,所以我们就通过签名和验证方案来实现软件授权激活码的生成和验证。主要原理其实也很简单,就是上面所说的,将验证信息用私钥签名后,将信息和签名打包成激活码,卖给用户。用户输入激活码后,客户端用公钥根据签名验证授权信息真实有效,予以授权。即使授权信息的格式相当于直接暴露,但由于无法伪造签名,伪造的授权信息无法通过验证,也能够保证安全性。 授权信息的作用是授权给固定对象,比如固定一个人和一台设备以及一定时间,防止激活码泄露后被滥用。 授权信息基本应该包括被授权人的基本信息(姓名、邮箱和账号等)、机器的基本信息(MAC地址,其他硬件的序列号,通过算法生成的机器码等)还有授权的有效时间(到期后不可使用)。除此以外还可以添加一些其他信息,比如对分价位的不同授权(基础版、普通版、高级版)做区分,对一些高级功能做限制等等。 本文代码主要使用C demo中授权信息由邮箱、有效日期和MAC地址构成,然后利用私钥进行加密。然后将授权信息长度 ```csharp public void GenerateLicense() { // 授权信息,为Json格式 var data = JsonSerializer.SerializeToUtf8Bytes(this); // 导入私钥 var rsa = new RSACryptoServiceProvider(); try { rsa.ImportFromPem(PrivateKey.AsSpan()); } catch (ArgumentException) { MessageBox.Show("私钥错误!"); return; } // 密钥由授权信息长度 // 用2个byte来存储信息长度 var dataLen = data.Length; var dataLenByte = new byte

阅读全文

写了一个OpenCC.NET

之前看到一个库叫,试了试它提供的感觉很不错,比一般的简繁傻转强多了(比如不会干出把“一”都转换成“壹”这种傻事),而且还支持常用词的转换(比如“内存”和“記憶體”)。 于是自己写了个C nuget上也可以直接下载包,搜索OpenCCNET(OpenCC.NET的这个带点的ID已经被人用了)即可。

阅读全文
公告栏

欢迎来到Fluent Blog,一个基于ASP.NET Core 5.0 MVC的博客。前往主站请访问www.cosineg.com

动态
@余弦G:

博客部署成功~

2021/4/22 上午12:15:08
90