要与该程序一起使用,您必须阅读代码,并且执行起来越容易,它看起来就越像自然语言-然后您就可以快速深入并专注于主要内容。
在最后两篇文章中,我展示了精心选择的单词有助于更好地理解所写内容的本质,但仅思考它们是不够的,因为每个单词都以两种形式存在:本身和作为句子的一部分。 直到我们在Thread.CurrentThread
的上下文中读取 CurrentThread
之前,它都不会重复。
因此,在音符和简单旋律的指导下,我们现在将了解什么是音乐。
循环目录
- 对象
- 动作和属性
- 文字编码
文字编码
大多数流畅的界面在设计时都侧重于外部而不是内部,因此它们非常易于阅读。 当然,这不是免费的:从某种意义上说,内容正在减弱。 假设,在FluentAssertions
包中, FluentAssertions
可以编写: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")
并且,相对于阅读, because
看起来很优雅,但是在Be()
, errorMessage
使用error
或errorMessage
参数。
我认为,这种豁免并不重要。 当我们同意代码是文本时,其组成部分不再属于自己:它们现在是某种通用的“以太”的一部分 。
我将通过示例展示如何将这些考虑变为经验。
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>
如果x
为null
Nullable<T>
调用x.Value
将引发异常。 如果仍然需要获取Value
,请使用x.GetValueOrDefault
:它是Value
或默认值。 大块头
表达式“或x或默认值”与简短的x.OrDefault
匹配。
int? x = null; var a = x.GetValueOrDefault();
使用OrDefault
和Or
,需要记住一件事:与运算符一起使用时.?
您不能编写类似x?.IsEnabled.Or(false)
,只能编写(x?.IsEnabled).Or(false)
(换句话说,如果左侧为null
,则.?
运算符将取消整个右侧)。
使用IEnumerable<T>
时可以应用模板:
IEnumerable<int> numbers = null;
Math.Min
和Math.Max
带有Or
的想法可以发展为数字类型。 假设您要从a
和b
取最大数。 然后我们写: Math.Max(a, b)
或a > b ? a : b
a > b ? a : b
。 这两个选项看起来都很熟悉,但是看起来却不是自然语言。
您可以将其替换为: a
或 a.Or(b).IfLess()
- 如果 a
小于 则 取 a
或 b
。 适用于以下情况:
Creature creature = ...; int damage = ...;
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 };
Regex
但是,与有时不正确地使用Regex
以及将其用于其他目的相比, string.Join
完全没有害处。 可以通过简单易读的文本获得帮助的地方,出于某种原因,首选过于复杂的条目。
让我们从一个简单的开始-确定一个字符串代表一组数字:
string id = ...;
另一种情况是从序列中找出字符串中是否至少有一个字符:
string text = ...;
任务越复杂,解决方案“模式”就越困难:将"HelloWorld"
的记录拆分为几个词"Hello World"
,则有人会用一种简单的算法来代替怪物:
string text = ...;
毫无疑问,正则表达式是有效且通用的,但是我想一眼就知道发生了什么。
Substring
并Remove
碰巧您需要从行的开头或结尾删除部分内容,例如,从path
-扩展名.txt
(如果有)中删除。
string path = ...;
同样, 操作和算法也消失了, 只剩下了一行,结尾没有.exe扩展名 。
由于Without
方法应该返回特定的WithoutExpression
,因此它们会请求另一个: path.Without("_").AtStart
和path.Without("Something").Anywhere
。 同样有趣的是,可以使用相同的单词构造另一个表达式: name.Without(charAt: 1)
-删除索引1处的字符并返回新行(在计算排列中很有用)。 而且可读性强!
Type.GetMethods
要使用反射获取某种类型的方法,请使用:
Type type = ...;
(对于GetFields
和GetProperties
。)
Directory.Copy
文件夹和文件的所有操作通常被概括为DirectoryUtils
, FileSystemHelper
。 它们实现文件系统旁路,清理,复制等。 但是在这里您可以提出更好的建议!
我们将文本“将所有文件从'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 = ...;
GetOrders
在诸如IOrdersRepository
对象中,经常发现其他方法: AddOrder
, RemoveOrder
。 前两个重复消失,并获得Add
和Remove
(具有相应的条目_orders.Add(order)
和_orders.Remove(order)
)。 使用GetOrders
更难以重命名Orders
。 让我们看看:
IOrdersRepository orders = ...;
应该注意的是,使用旧的_ordersRepository
或GetOrderById
调用中的重复并不是那么明显,因为我们正在使用存储库!
诸如One
, All
类的名称适用于表示许多接口的许多接口。 说,在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]
); - 返回默认值(未实现,但通常写
GetValueOrDefault
或TryGetValue
); - 返回其他内容(未实现,但我希望
GetValueOrOther
); - 将指定的值写入字典并返回(未实现,但
GetOrAdd
)。
表达式收敛于“ 取一些X,如果不取X则取Y ”。 另外,与_ordersRepository
,我们将字典变量itemsDictionary
而不是itemsDictionary
。
然后对于“带一些X”部分 ,调用表items.One(withKey: X)
理想的,返回一个带有四个结尾的结构:
Dictionary<int, Item> items = ...; int id = ...;
Assembly.GetTypes
让我们看一下如何在程序集中创建所有现有的类型T
实例:
因此,有时,静态类的名称是表达式的开头 。
在NUnit中可以找到类似的内容: Assert.That(2 + 2, Is.EqualTo(4))
- Is
并且不被认为是自给自足的类型。
Argument.ThrowIfNull
现在让我们看一下前提条件检查:
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 = ...;
异国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.Of
, Is.EqualTo
。 - 作为表达式一部分的方法(
using static
)是Ensure(that: x)
, items.All(Weapons)
。
因此,外部的和预期的成为最重要的。 首先考虑一下,然后考虑其特定的形式,只要代码被读为文本,就没有那么重要了。 由此可见,法官与其说是语言,不如说是他的品味-他确定item.GetValueOrDefault
和item.OrDefault
之间的区别。
结语
哪个更好,更清楚,但不是可行的方法,或者是可行的,却难以理解? 没有家具和房间的雪白城堡,还是路易十四风格的带沙发棚? 没有引擎或or吟的驳船以及没有人能使用的量子计算机的豪华游艇?
极地答案不适合,但也“在中间的某个地方” 。
我认为,这两个概念是密不可分的:仔细选择书籍的封面,我们会怀疑文本中的错误,反之亦然。 我不希望甲壳虫乐队播放低质量的音乐,但也应将它们称为MusicHelper 。
另一件事是,在开发过程中处理单词是一件被低估的,不寻常的事情,因此仍然需要某种极端的判断力。 这个周期是形式和图片的极限。
谢谢大家的关注!
参考文献
任何有兴趣查看更多示例的人都可以在我的GitHub上找到,例如,在Pocket.Common
库中。 (不适用于全球和通用)