该代码是活的还是死的。 第三部分。 文字编码

要与该程序一起使用,您必须阅读代码,并且执行起来越容易,它看起来就越像自然语言-然后您就可以快速深入并专注于主要内容。


在最后两篇文章中,我展示了精心选择的单词有助于更好地理解所写内容的本质,但思考它们是不够的,因为每个单词都以两种形式存在:本身和作为句子的一部分。 直到我们在Thread.CurrentThread的上下文中读取 CurrentThread之前,它都不会重复。


因此,在音符和简单旋律的指导下,我们现在将了解什么是音乐。


循环目录


  1. 对象
  2. 动作和属性
  3. 文字编码

文字编码


大多数流畅的界面在设计时都侧重于外部而不是内部,因此它们非常易于阅读。 当然,这不是免费的:从某种意义上说,内容正在减弱。 假设,在FluentAssertions包中, FluentAssertions可以编写: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")并且,相对于阅读, because看起来很优雅,但是在Be()errorMessage使用errorerrorMessage参数。


我认为,这种豁免并不重要。 当我们同意代码是文本时,其组成部分不再属于自己:它们现在是某种通用的“以太”的一部分


我将通过示例展示如何将这些考虑变为经验。


Interlocked


让我想起您Interlocked.CompareExchange(ref x, newX, oldX)Interlocked情况,我们将Interlocked.CompareExchange(ref x, newX, oldX)转换为Interlocked.CompareExchange(ref x, newX, oldX) Atomically.Change(ref x, from: oldX, to: newX) ,使用明确的方法和参数名称。


ExceptWith


ISet<>类型具有一个名为ExceptWith的方法。 如果您查看类似于items.ExceptWith(other)的呼叫,您将不会立即意识到发生了什么。 但是,您只需编写: items.Exclude(other) ,一切都准备就绪。


GetValueOrDefault


使用Nullable<T>如果xnull Nullable<T>调用x.Value将引发异常。 如果仍然需要获取Value ,请使用x.GetValueOrDefault :它是Value或默认值。 大块头


表达式“或x或默认值”与简短的x.OrDefault匹配。


 int? x = null; var a = x.GetValueOrDefault(); // ,  .  . var b = x.OrDefault(); //  —  ,   . var c = x.Or(10); //     . 

使用OrDefaultOr ,需要记住一件事:与运算符一起使用时.? 您不能编写类似x?.IsEnabled.Or(false) ,只能编写(x?.IsEnabled).Or(false) (换句话说,如果左侧为null ,则.?运算符将取消整个右侧)。


使用IEnumerable<T>时可以应用模板:


 IEnumerable<int> numbers = null; // . var x = numbers ?? Enumerable.Empty<int>(); //   . var x = numbers.OrEmpty(); 

Math.MinMath.Max


带有Or的想法可以发展为数字类型。 假设您要从ab取最大数。 然后我们写: Math.Max(a, b)a > b ? a : b a > b ? a : b 。 这两个选项看起来都很熟悉,但是看起来却不是自然语言。


您可以将其替换为: a a.Or(b).IfLess() - 如果 a 小于 a b 。 适用于以下情况:


 Creature creature = ...; int damage = ...; //   . creature.Health = Math.Max(creature.Health - damage, 0); // Fluent. creature.Health = (creature.Health - damage).Or(0).IfGreater(); //   : creature.Health = (creature.Health - damage).ButNotLess(than: 0); 

string.Join


有时您需要将序列组装成字符串,并用空格或逗号分隔元素。 为此,例如,使用string.Join ,例如: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".


一个简单的“将逗号分隔”可能突然变成“将逗号添加到列表中的每个数字” –这当然不是文本代码。


 var numbers = new [] { 1, 2, 3 }; // ""    —  . var x = string.Join(", ", numbers); //    — ! var x = numbers.Separated(with: ", "); 

Regex


但是,与有时不正确地使用Regex以及将其用于其他目的相比, string.Join完全没有害处。 可以通过简单易读的文本获得帮助的地方,出于某种原因,首选过于复杂的条目。


让我们从一个简单的开始-确定一个字符串代表一组数字:


 string id = ...; // ,  . var x = Regex.IsMatch(id, "^[0-9]*$"); // . var x = id.All(x => x.IsDigit()); // ! var x = id.IsNumer(); 

另一种情况是从序列中找出字符串中是否至少有一个字符:


 string text = ...; //   . var x = Regex.IsMatch(text, @"["<>[]'"); //   . ( .) var x = text.ContainsAnyOf('"', '<', '>', '[', ']', '\''); //  . var x = text.ContainsAny(charOf: @"["<>[]'"); 

任务越复杂,解决方案“模式”就越困难:将"HelloWorld"的记录拆分为几个词"Hello World" ,则有人会用一种简单的算法来代替怪物:


 string text = ...; //   -   . var x = Regex.Replace(text, "([az](?=[AZ])|[AZ](?=[AZ][az]))", "$1 "); //  . var x = text.PascalCaseWords().Separated(with: " "); //   . var x = text.AsWords(eachStartsWith: x => x.IsUpper()).Separated(with: " "); 

毫无疑问,正则表达式是有效且通用的,但是我想一眼就知道发生了什么。


SubstringRemove


碰巧您需要从行的开头或结尾删除部分内容,例如,从path -扩展名.txt (如果有)中删除。


 string path = ...; //    . var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path; //   . var x = path.Without(".exe").AtEnd; 

同样, 操作算法也消失了, 只剩下了一行,结尾没有.exe扩展名


由于Without方法应该返回特定的WithoutExpression ,因此它们会请求另一个: path.Without("_").AtStartpath.Without("Something").Anywhere 。 同样有趣的是,可以使用相同的单词构造另一个表达式: name.Without(charAt: 1) -删除索引1处的字符并返回新行(在计算排列中很有用)。 而且可读性强!


Type.GetMethods


要使用反射获取某种类型的方法,请使用:


 Type type = ...; //   `Get` ,   `|`.     . var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // ,  . `Or`   , . var x = type.Methods(_ => _.Instance.Public.Or.NonPublic); 

(对于GetFieldsGetProperties 。)


Directory.Copy


文件夹和文件的所有操作通常被概括为DirectoryUtilsFileSystemHelper 。 它们实现文件系统旁路,清理,复制等。 但是在这里您可以提出更好的建议!


我们将文本“将所有文件从'D:\ Source'复制到'D:\ Target'”显示为代码"D:\\Source".AsDirectory().Copy().Files.To("D:\\Target") AsDirectory() -从string返回DirectoryInfo ,而Copy() -创建一个CopyExpression实例,该实例描述用于构造表达式的唯一API(例如,您不能调用Copy().Files.Files )。 然后机会打开了,不是复制所有文件,而是Copy().Files.Where(x => x.IsNotEmpty)一些文件: Copy().Files.Where(x => x.IsNotEmpty)


GetOrderById


在第二篇文章中,我写道IUsersRepository.GetUser(int id)是多余的,更好的是IUsersRepository.User(int id) 。 因此,在类似的IOrdersRepository我们没有IOrdersRepository GetOrderById(int id) ,而是Order(int id) 。 但是,在另一个示例中,建议将此类存储库的变量称为_ordersRepository而不是_orders


两种更改本身都很好,但是它们在阅读上下文中并没有加在一起:调用_orders.Order(id)看起来很冗长。 _orders.Get(id)是可能的,但是订单失败,我们只想指定一个具有这样的标识符的订单。 因此, 一个One


 IOrdersRepository orders = ...; int id = ...; //   . var x = orders.GetOrderById(id); //      : var x = orders.Order(id); //     ,    . var x = orders.One(id); //    : var x = orders.One(with: id); 

GetOrders


在诸如IOrdersRepository对象中,经常发现其他方法: AddOrderRemoveOrder 。 前两个重复消失,并获得AddRemove (具有相应的条目_orders.Add(order)_orders.Remove(order) )。 使用GetOrders更难以重命名Orders 。 让我们看看:


 IOrdersRepository orders = ...; //   . var x = orders.GetOrders(); //  `Get`,  . var x = orders.Orders(); // ! var x = orders.All(); 

应该注意的是,使用旧的_ordersRepositoryGetOrderById调用中的重复并不是那么明显,因为我们正在使用存储库!


诸如OneAll类的名称适用于表示许多接口的许多接口。 说,在GitHub API的著名实现中octokit获取所有用户存储库看起来像gitHub.Repository.GetAllForUser("John") ,尽管更合乎逻辑gitHub.Users.One("John").Repositories.All 。 在这种情况下,获得一个存储库分别是gitHub.Repository.Get("John", "Repo")而不是明显的gitHub.Users.One("John").Repositories.One("Repo") 。 第二种情况看起来更长,但是在内部是一致的,反映了平台。 另外,使用扩展方法,可以将其缩短为gitHub.User("John").Repository("Repo")


Dictionary.TryGetValue


从字典中获取值分为几种情况,仅在找不到密钥的情况下需要做的事情不同:


  • 抛出错误( dictionary[key] );
  • 返回默认值(未实现,但通常写GetValueOrDefaultTryGetValue );
  • 返回其他内容(未实现,但我希望GetValueOrOther );
  • 将指定的值写入字典并返回(未实现,但GetOrAdd )。

表达式收敛于“ 取一些X,如果不取X则取Y ”。 另外,与_ordersRepository ,我们将字典变量itemsDictionary而不是itemsDictionary


然后对于“带一些X”部分 ,调用表items.One(withKey: X)理想的,返回一个带有四个结尾的结构:


 Dictionary<int, Item> items = ...; int id = ...; //  ,   : var x = items.GetValueOrDefault(id); var x = items[id]; var x = items.GetOrAdd(id, () => new Item()); //    : var x = items.One(with: id).OrDefault(); var x = items.One(with: id).Or(Item.Empty); var x = items.One(with: id).OrThrow(withMessage: $"Couldn't find item with '{id}' id."); var x = items.One(with: id).OrNew(() => new Item()); 

Assembly.GetTypes


让我们看一下如何在程序集中创建所有现有的类型T实例:


 // . var x = Assembly .GetAssembly(typeof(T)) .GetTypes() .Where(...) .Select(Activator.CreateInstance); // "" . var x = TypesHelper.GetAllInstancesOf<T>(); // . var x = Instances.Of<T>(); 

因此,有时,静态类的名称是表达式的开头


在NUnit中可以找到类似的内容: Assert.That(2 + 2, Is.EqualTo(4)) - Is并且不被认为是自给自足的类型。


Argument.ThrowIfNull


现在让我们看一下前提条件检查:


 //  . Argument.ThrowIfNull(x); Guard.CheckAgainstNull(x); // . x.Should().BeNotNull(); // ,  ...  ? Ensure(that: x).NotNull(); 

Ensure.NotNull(argument) -不错,但英语不太好。 另一件事是上面编写的Ensure(that: x).NotNull() 。 如果只有...


顺便说一句,你可以! 我们编写Contract.Ensure(that: argument).IsNotNull()using static导入Contract的类型。 这样就获得了Ensure(that: type).Implements<T>()Ensure(that: type).Implements<T>()Ensure(that: number).InRange(from: 5, to: 10)等。


静态导入的想法打开了许多门。 出于以下原因的一个美丽示例:代替items.Remove(x)编写Remove(x, from: items) 。 但是令人好奇的是减少了返回函数的enum和属性。


 IItems items = ...; // . var x = items.All(where: x => x.IsWeapon); //  . // `ItemsThatAre.Weapons`  `Predicate<bool>`. var x = items.All(ItemsThatAre.Weapons); // `using static`  !  . var x = items.All(Weapons); 

异国Find


在C#7.1和更高版本中,您不能写Find(1, @in: items) ,而不能写Find(1, in items) ,其中Find定义为Find<T>(T item, in IEnumerable<T> items) 。 这个例子是不切实际的,但是表明在努力提高可读性方面,所有手段都是好的。


合计


在这一部分中,我研究了提高代码可读性的几种方法。 所有这些都可以概括为:


  • 作为表达式一部分的命名参数为 Should().Be(4, because: "")Atomically.Change(ref x, from: oldX, to: newX)
  • 一个简单的名称而不是技术详细信息Separated(with: ", ")Exclude
  • 作为变量一部分的方法x.OrDefault()x.Or(b).IfLess()x.Or(b).IfLess() orders.One(with: id)orders.All
  • 作为表达式一部分的方法path.Without(".exe").AtEnd
  • 作为表达式一部分的类型Instances.OfIs.EqualTo
  • 作为表达式一部分的方法( using staticEnsure(that: x)items.All(Weapons)

因此,外部的和预期的成为最重要的。 首先考虑一下,然后考虑其特定的形式,只要代码被读为文本,就没有那么重要了。 由此可见,法官与其说是语言,不如说是他的品味-他确定item.GetValueOrDefaultitem.OrDefault之间的区别。


结语


哪个更好,更清楚,但不是可行的方法,或者是可行的,却难以理解? 没有家具和房间的雪白城堡,还是路易十四风格的带沙发棚? 没有引擎或or吟的驳船以及没有人能使用的量子计算机的豪华游艇?


极地答案不适合,但也“在中间的某个地方”


我认为,这两个概念是密不可分的:仔细选择书籍的封面,我们会怀疑文本中的错误,反之亦然。 我不希望甲壳虫乐队播放低质量的音乐,但也应将它们称为MusicHelper


另一件事是,在开发过程中处理单词是一件被低估的,不寻常的事情,因此仍然需要某种极端的判断力。 这个周期是形式和图片的极限。


谢谢大家的关注!


参考文献


任何有兴趣查看更多示例的人都可以在我的GitHub上找到,例如,在Pocket.Common库中。 (不适用于全球和通用)

Source: https://habr.com/ru/post/zh-CN447830/


All Articles