处理C#中“循环初始化”的其他方法
-
02-10-2019 - |
题
首先,我要说的是,我同意在现代编程语言中的更高级别的构造基本上是无关紧要的,当可用的替代品可用时不应使用。
我最近重新阅读了史蒂夫·麦康奈尔(Steve McConnell)的代码的原始版本,并忘记了他对常见编码问题的建议。几年前,我曾经读过它,当时我刚开始开始时,并不认为我意识到食谱将有多有用。编码问题是:执行循环时,通常需要执行部分循环以初始化状态,然后使用其他逻辑执行循环,并使用相同的初始化逻辑结束每个循环。一个具体示例正在实现字符串。
我认为每个人的第一个问题就是这个问题。假设将附录方法定义为将参数添加到您的返回值中。
bool isFirst = true;
foreach (var element in array)
{
if (!isFirst)
{
append(delimiter);
}
else
{
isFirst = false;
}
append(element);
}
注意:对此有轻微的优化是删除其他并将其放在循环的末端。分配通常是单个指令,等同于其他指令,并将基本块的数量减少1,并增加主零件的基本块大小。结果是在每个循环中执行条件以确定是否应添加定界符。
我还看到并使用了其他方法来处理这个常见的循环问题。您可以先执行循环外的初始元素代码,然后执行从第二个元素到末端的循环。您还可以更改逻辑以始终附加元素,然后再插入定界符,一旦循环完成,您就可以简单地删除添加的最后一个定界符。
后一个解决方案往往是我最喜欢的解决方案,因为它不会复制任何代码。如果初始化序列的逻辑发生了变化,则不必记住在两个地方修复它。但是,它确实需要额外的“工作”来做些事情然后撤消它,从而导致至少额外的CPU周期,在许多情况下,例如我们的字符串。Join示例也需要额外的记忆。
然后我很高兴阅读这个结构
var enumerator = array.GetEnumerator();
if (enumerator.MoveNext())
{
goto start;
do {
append(delimiter);
start:
append(enumerator.Current);
} while (enumerator.MoveNext());
}
这里的好处是,您没有重复的代码,也没有其他工作。您可以在执行第一个循环的一半开始循环,这就是您的初始化。您仅限于在构造时模拟其他循环,但是翻译很容易,并且阅读并不困难。
所以,现在的问题。我很高兴地尝试将其添加到我正在处理的一些代码中,发现它不起作用。在C,C ++,碱性中的功能很好,但事实证明,您不能跳到不是父范围的其他词汇范围内的标签。我很失望。因此,我想知道,在C#中处理这个非常常见的编码问题(我主要是在字符串生成中)的最佳方法是什么?
也许对要求更具体:
- 不要重复代码
- 不要做不必要的工作
- 不要比其他代码慢2或3倍
- 可读
我认为可读性是我所说的食谱唯一可能会遭受痛苦的事情。但是,它在C#中不起作用,那么下一个最好的事情是什么?
* 编辑 *由于某些讨论,我更改了性能标准。性能通常不是这里的限制因素,因此更正确的目标应该是不要不合理,不要成为有史以来最快的。
我不喜欢我建议的替代实现的原因是因为它们要么重复代码,要么为更改一部分而不是另一部分或我选择的一个零件留出空间你刚才做了。特别是通过弦操作,通常这通常会让您因一个错误而开放,或者无法解释一个空数组,并试图撤消没有发生的事情。
解决方案
对于您的具体示例,有一个标准解决方案: string.Join
. 。这样可以正确添加定界符,因此您不必自己编写循环。
如果您真的想自己编写一种方法,则可以使用以下方法:
string delimiter = "";
foreach (var element in array)
{
append(delimiter);
append(element);
delimiter = ",";
}
这应该是相当有效的,我认为阅读是合理的。常规字符串“”已被实施,因此这不会导致每次迭代中创建一个新的字符串。当然,如果性能对于您的应用程序至关重要,则应该基准测试而不是猜测。
其他提示
我个人喜欢Mark Byer的选项,但是您总是可以为此编写自己的通用方法:
public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source,
Action<T> firstAction,
Action<T> subsequentActions)
{
using (IEnumerator<T> iterator = source.GetEnumerator())
{
if (iterator.MoveNext())
{
firstAction(iterator.Current);
}
while (iterator.MoveNext())
{
subsequentActions(iterator.Current);
}
}
}
那是相对简单的...给特别的 最后的 动作有些困难:
public static void IterateWithSpecialLast<T>(this IEnumerable<T> source,
Action<T> allButLastAction,
Action<T> lastAction)
{
using (IEnumerator<T> iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
{
return;
}
T previous = iterator.Current;
while (iterator.MoveNext())
{
allButLastAction(previous);
previous = iterator.Current;
}
lastAction(previous);
}
}
编辑:当您的评论与此有关的表现有关,我将在此答案中重申我的评论:虽然这个一般问题是相当普遍的,但它是 不是 通常,它是一种性能瓶颈,值得微观挑选。确实,我不记得曾经遇到过循环机械变成瓶颈的情况。我确定它会发生,但是 那 不是“常见”。如果我遇到过它,我将特别限制该特定代码,最好的解决方案将取决于 确切地 代码需要做什么。
但是,总的来说,我重视可读性和可重复性 很多 不仅仅是微选。
您已经愿意放弃foreach。因此,这应该是合适的:
using (var enumerator = array.GetEnumerator()) {
if (enumerator.MoveNext()) {
for (;;) {
append(enumerator.Current);
if (!enumerator.MoveNext()) break;
append(delimiter);
}
}
}
您当然可以创建一个 goto
C#中的解决方案(注意:我没有添加 null
检查):
string Join(string[] array, string delimiter) {
var sb = new StringBuilder();
var enumerator = array.GetEnumerator();
if (enumerator.MoveNext()) {
goto start;
loop:
sb.Append(delimiter);
start: sb.Append(enumerator.Current);
if (enumerator.MoveNext()) goto loop;
}
return sb.ToString();
}
为您 具体的 例如,这对我来说看起来很直截了当(这是您描述的解决方案之一):
string Join(string[] array, string delimiter) {
var sb = new StringBuilder();
foreach (string element in array) {
sb.Append(element);
sb.Append(delimiter);
}
if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length;
return sb.ToString();
}
如果您想获得功能,可以尝试使用这种折叠方法:
string Join(string[] array, string delimiter) {
return array.Aggregate((left, right) => left + delimiter + right);
}
虽然它的读物真的很好,但它没有使用 StringBuilder
, ,所以你可能想滥用 Aggregate
有点使用它:
string Join(string[] array, string delimiter) {
var sb = new StringBuilder();
array.Aggregate((left, right) => {
sb.Append(left).Append(delimiter).Append(right);
return "";
});
return sb.ToString();
}
或者您可以使用此(从这里借用其他答案借用):
string Join(string[] array, string delimiter) {
return array.
Skip(1).
Aggregate(new StringBuilder(array.FirstOrDefault()),
(acc, s) => acc.Append(delimiter).Append(s)).
ToString();
}
有时我使用linq .First()
和 .Skip(1)
为了解决这个问题...这可以提供相对干净(非常可读性)的解决方案。
使用您的示例,
append(array.First());
foreach(var x in array.Skip(1))
{
append(delimiter);
append (x);
}
假设数组中至少有一个元素,如果要避免这种情况,可以添加一个简单的测试。
使用F#将是另一个建议:-)
您可以通过多种方式“”围绕加倍代码,但是在大多数情况下,重复的代码比可能的解决方案要丑陋/危险得多。您引用的“ goto”解决方案似乎对我来说并不是一个改进 - 我真的不认为您真的可以通过使用它来获得任何重要的(紧凑,可读性或效率),而您会增加程序员遇到错误的风险在代码一生中的某个时候。
总的来说,我倾向于采取这种方法:
- 第一个(或最后)动作的特殊情况
- 其他动作的循环。
这可以消除通过检查循环是否每次都在第一次迭代中进行的效率低下,并且真的很容易理解。对于非平凡的情况,使用委托或辅助方法应用该操作可以最大程度地减少代码重复。
或我有时使用效率不重要的另一种方法:
- 循环,并测试字符串是否为空,以确定是否需要定界符。
这可以写成比GOTO方法更紧凑,更可读,并且不需要任何额外的变量/存储/测试来检测“特殊情况” Iteraiton。
但是我认为马克·拜尔斯(Mark Byers)的方法是您特定示例的一个很好的干净解决方案。
我更喜欢 first
可变方法。它可能不是最干净,但最有效的方法。或者您可以使用 Length
您将其附加的东西与零进行比较。搭配得很好 StringBuilder
.
为什么不移动在循环外处理第一个元素?
StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
sb.append(",")
sb.append(elem)
}
如果您想走功能路由,则可以定义字符串。像Linq构造一样加入可以重复使用的linq构造。
就个人而言,我几乎总是会为保存一些操作码执行而进行代码清晰度。
例如:
namespace Play
{
public static class LinqExtensions {
public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner)
{
U joined = default(U);
bool first = true;
foreach (var item in list)
{
if (first)
{
joined = initializer(item);
first = false;
}
else
{
joined = joiner(joined, item);
}
}
return joined;
}
}
class Program
{
static void Main(string[] args)
{
List<int> nums = new List<int>() { 1, 2, 3 };
var sum = nums.JoinElements(a => a, (a, b) => a + b);
Console.WriteLine(sum); // outputs 6
List<string> words = new List<string>() { "a", "b", "c" };
var buffer = words.JoinElements(
a => new StringBuilder(a),
(a, b) => a.Append(",").Append(b)
);
Console.WriteLine(buffer); // outputs "a,b,c"
Console.ReadKey();
}
}
}