标签:编程

Effective C# 阅读笔记(三)

为了提升一下自己的姿势水平,买了 和 两本书,想一边阅读一边强迫自己做点笔记(主要是自己的理解),不然怕是看了就忘。另外手头还有一本《深入解析C 之前为了找实习(今年真难啊),花了很多时间在了刷LeetCode和背八股文上。现在离去实习也没多久了,啥也不会过去不太好,所以后续应该也要花相当时间在学Java上。今天这篇是 的第三章。 泛型还是一个比较方便和重要的特性。Java的泛型基于类型擦除,也就是编译器会保证类型安全,但实际运行时类型都被擦除了,所有不同都是同一个类型(个人的理解,有错误请务必指出)。而C 书中主要是约束条件限制得太宽,那就很难使用,因为缺少信息,很多操作不能使用,比如 如果设定了的约束条件,那么在内部对于类型的变量就可以使用,否则就只能调用。 同样,如果设置得过严,那就失去了泛型的优势,比如: 对象按理说应该也能完成上面循环输出的操作,但实际上却无法使用,因为并没有实现接口。而操作只要实现了就能使用,因此把约束换为更加合理。 另外书中提醒还有约束也要注意使用,因为有的时候代码中的可以用替代。其中对于值类型,返回的就是值类型的默认值,所以和没区别(注意C 总的来说,泛型就是为了在各种场合能够广泛使用,所以要仔细考虑约束条件。约束条件太宽,有很多特性难以使用;约束条件太严,使用场合就受限。因此要仔细考虑每一条约束条件,只保留必要的条件。 放宽类型参数的限制(比如让输入参数的类型为接口,而非一个具体类型),可以让程序更加通用和泛化,但是这又会导致实现的时候无法使用一些具体类型才拥有的特性。因此可以通过运行期类型检查来检测参数的运行时类型,如果是一些特定类型则使用其的某些特性,以此达到提升程序性能的目的。 书中举了个作者自己写的例子,但我觉得直接看的源码就好,有一个参数为类型的构造函数: csharp public sealed class EngineDriver where T : IEngine { // 私有T类型的成员变量 private T _driver; // 通过依赖注入获取外部的T类型的变量 public EngineDriver(T driver) = public void GetThingsDone() = } csharp IEnumerable a = new List() { " IEnumerable b = a; csharp // 协变(T仅作输出) public interface IEnumerable : IEnumerable { IEnumerator GetEnumerator(); } // 逆变(T仅作输入) public delegate void Action(T obj); // 不变(T既做输入也做输出) public interface IList : ICollection, IEnumerable, IEnumerable { ... } csharp // 协变 IEnumerable a = new List() { " IEnumerable b = a; // c是object var c = b.First(); csharp // 逆变 var a = new Action(Console.WriteLine); Action b = a; // 输入字符串 b(" csharp // 不变 IList a = new List() { " // 尝试协变,但是本句无法通过编译 IList b = a; b.Add(new object()); csharp // 不变 IList a = new List() { new object() }; // 尝试逆变,但是本句无法通过编译 IList b = a; var element = b[0]; csharp public delegate TResult Func(T arg); csharp // 支持逆变 interface IA { } class A : IA { } // 支持协变 interface IB { // 好像类型参数明明只做了输入而不是输出? void Add(IA foo); } class ListA : IB { // 一个List用于存IA对象 private List // 往List里添加IA元素 public void Add(IA a) { _list.Add(a); } } csharp // 协变 IB b = new ListA(); IA a = new A(); // 其实这里a发生了逆变 b.Add(a); csharp public class Adder { public T Val { get; set; } public Adder(T val) { Val = val; } // 把两个Adder的Val相加,得到一个新Adder // 具体两个T的相加是怎么实现的,交由开发者自己把委托传进来 public Adder Add(Adder other, Func addFunc) { return new Adder(addFunc(Val, other.Val)); } } csharp var adder1 = new Adder(1); var adder2 = new Adder(2); // 具体两个参数如何相加靠开发者自由实现 // 我这里通过lambda表达式写了个最简单的实现 adder csharp public class TestList { private readonly List _list = new List(); private readonly Func _convertFunc; // 交由其他开发者实现转换的细节 public TestList(Func convertFunc) { _convertFunc = convertFunc; } // 输出T2类型的元素 public T2 this[int index] = // 输入T1类型的元素,转换成T2类型存储 public void Add(T1 item) { _list.Add(_convertFunc(item)); } } csharp // 传入转换的具体实现 var list = new TestList(int.Parse); list.Add("0"); int element = list[0]; csharp // 父类 class Shape { } // 接口 interface IColor { } // 子类 class Circle : Shape, IColor { } csharp // 泛型 void Print(T t) { Console.WriteLine("Generic"); } // 重载(父类) void Print(Shape shape) { Console.WriteLine("Shape"); } // 重载(接口) void Print(IColor color) { Console.WriteLine("IColor"); } csharp // 子类实例 var circle = new Circle(); // 转型到父类 Shape shape = circle; // 转型到接口 IColor color = circle; // 调用的是Print(T t) Print(circle); // 调用的是Print(Shape shape) Print(shape); // 调用的是Print(IColor color) Print(color); csharp void Print(T t) { if (t is Shape) { Console.WriteLine("Shape"); } else if (t is IColor) { Console.WriteLine("IColor"); } else { Console.WriteLine("Generic"); } } csharp // 泛型类 public static class Utils { public static T Max(T left, T right) = Comparer.Default.Compare(left, right) < 0 ? right : left; public static T Min(T left, T right) = Comparer.Default.Compare(left, right) < 0 ? left : right; } csharp // 能用,但是手动必须指明类型 // 生成了两份Utils,且它们各自都没用到的Min()也还是被生成了 Utils.Max(7, 12); Utils.Max("lty", " csharp public static class Utils { // 泛型方法 public static T Max(T left, T right) = Comparer.Default.Compare(left, right) < 0 ? right : left; // 而且还可以为一些特定类型设置更高效的重载方法 public static int Max(int left, int right) = public static T Min(T left, T right) = Comparer.Default.Compare(left, right) < 0 ? left : right; public static int Min(int left, int right) = } csharp // 编译器会自动推断类型 Utils.Max(7, 12); Utils.Max("lty", " csharp public class CommaSeparatedListBuilder { private readonly StringBuilder _builder = new StringBuilder(); public void Add(IEnumerable items) { foreach (var item in items) { if (_builder.Length { _builder.Append(", "); } _builder.Append(item); } } public override string ToString() = } csharp var builder = new CommaSeparatedListBuilder(); builder.Add(new List() { 1, 2 }); builder.Add(new List() { "3", "4" }); csharp public interface IEnumerable : IEnumerable { IEnumerator GetEnumerator(); } csharp // 扩展方法 public static int Count(this IEnumerable source) { if (source == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source); if (source is ICollection sources) return sources.Count; // 我看源码里挺多Provider接口的,不知道是什么,文档上也找不到 if (source is IIListProvider ilistProvider) return ilistProvider.GetCount(false); if (source is ICollection collection) return collection.Count; int num = 0; using (IEnumerator enumerator = source.GetEnumerator()) { while (enumerator.MoveNext()) checked { } return num; } csharp public interface IShape { public string Shape { get; set; } public void Print() { Console.WriteLine(Shape); } } public interface IColor { public string Color { get; set; } public void Print() { Console.WriteLine(Color); } } public class BlueCircle : IShape, IColor { public string Shape { get; set; } = "Circle"; public string Color { get; set; } = "Blue"; } csharp var blueCircle = new BlueCircle(); // 无法编译,提示BlueCircle中找不到Print()的定义 blueCircle.Print(); // 输出Circle ((IShape)blueCircle).Print(); // 输出Blue ((IColor)blueCircle).Print(); csharp public class BlueCircle : IShape, IColor { public string Shape { get; set; } = "Circle"; public string Color { get; set; } = "Blue"; public void Print() { Console.WriteLine($"{Color}{Shape}"); } } csharp var blueCircle = new BlueCircle(); // 输出BlueCircle BlueCircle.Print(); // 输出BlueCircle ((IShape)blueCircle).Print(); // 输出BlueCircle ((IColor)blueCircle).Print(); csharp private static IEnumerable ConvertBy(this IEnumerable phrases, params IDictionary[] dictionaries) { return phrases.Select(phrase = { ... // 根据字典进行转换 } } csharp public static string HansToTWWithPhrase(string text) { // 分词 var phrases = ZhSegment.Segment(text); // 链式调用扩展方法 return phrases.ConvertBy(ZhDictionary.STPhrases, ZhDictionary.STCharacters) .ConvertBy(ZhDictionary.TWPhrases) .ConvertBy(ZhDictionary.TWVariants) // Join()也是我写的扩展方法,其实就是调用string.Join()把词组重新合并成句子 .Join(); } csharp return string.Join(string.Empty, ConvertBy(ConvertBy(ConvertBy(phrases, ZhDictionary.STPhrases, ZhDictionary.STCharacters), ZhDictionary.TWPhrases), ZhDictionary.TWVariants)); ``` 光看看都觉得崩溃。 第三章完。

阅读全文

更新了OpenCC.NET,加上记Nuget打包的一个坑

这两天有点空,把重构和优化了一下,版本号也正式来到了 xml True True content\Dictionary\ True True content\JiebaResource\ ``` 这次确实只打包一份资源文件了。 但搞笑的是又不能自动把两个文件夹复制到程序输出目录了。所以最后我还是只能选择去掉指定路径,让它自动给我打包两份资源文件上去。虽然大小成了两倍,但至少能用不是嘛。只能说属实是有点坑了。

阅读全文

Effective C# 阅读笔记(二)

为了提升一下自己的姿势水平,买了 和 两本书,想一边阅读一边强迫自己做点笔记(主要是自己的理解),不然怕是看了就忘。另外手头还有一本《深入解析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没有自带选择文件夹窗口)。

阅读全文
公告栏

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

动态
@余弦G:

博客部署成功~

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