为啥我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ?
Posted
技术标签:
【中文标题】为啥我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ?【英文标题】:Why should I use foreach instead of for (int i=0; i<length; i++) in loops?为什么我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ? 【发布时间】:2011-05-21 23:03:03 【问题描述】:在 C# 和 Java 中循环似乎很酷的方式是使用 foreach 而不是 C 风格的 for 循环。
有什么理由让我更喜欢这种风格而不是 C 风格?
我对这两个案例特别感兴趣,但请尽可能多地解决您需要解释您的观点的案例。
我希望对列表中的每个项目执行操作。 我正在搜索列表中的项目,并希望在找到该项目时退出。【问题讨论】:
顺便说一句,“至少解决这两种情况”真的让你的问题听起来像家庭作业:) 以防万一你没有意识到...... 另见***.com/questions/365615/…、***.com/questions/1124753/for-vs-foreach-loop-in-c和***.com/questions/4364469/… 这个问题一直以各种形式被问到,并且暗示(例如,有人建议重构为“很酷的方式”)甚至更频繁;但这是我在编程中最喜欢的问题之一。 +1。 @P.Brian.Mackey 正是因为性能在这里不是真正关心的问题,这意味着我们可以谈论和解释风格。有些人真的只是不“明白”。他们花了很长时间与 C 和 C++ 搏斗,以至于他们实际上忘记了在这些语言中执行循环的方式在任何有意义的意义上实际上并不是“自然”的。 (我将其描述为一种脑损伤,如果只是因为有时你需要那种修辞来表达观点)。 OP 努力不去评判,并提出了一个有趣的风格问题;恕我直言,这是一个很好的问题。 @stuart 嗯。我没有考虑过硬件角度。任何回答的人的额外信用。 +1 【参考方案1】:foreach 对于实现大量收集来说要慢一个数量级。
我有证据。这是我的发现
我使用以下简单的分析器来测试它们的性能
static void Main(string[] args)
DateTime start = DateTime.Now;
List<string> names = new List<string>();
Enumerable.Range(1, 1000).ToList().ForEach(c => names.Add("Name = " + c.ToString()));
for (int i = 0; i < 100; i++)
//For the for loop. Uncomment the other when you want to profile foreach loop
//and comment this one
//for (int j = 0; j < names.Count; j++)
// Console.WriteLine(names[j]);
//for the foreach loop
foreach (string n in names)
Console.WriteLine(n);
DateTime end = DateTime.Now;
Console.WriteLine("Time taken = " + end.Subtract(start).TotalMilliseconds + " milli seconds");
我得到了以下结果
耗时 = 11320.73 毫秒(for 循环)
耗时 = 11742.3296 毫秒(foreach 循环)
【讨论】:
虽然您进行时间测量的方式没有任何问题,但您应该考虑使用专为此任务设计的 Stopwatch 对象。 我希望看到使用秒表的后续行动 您还应该对多次重复的结果进行平均 我看不出 11742 毫秒比 11321 毫秒“慢一个数量级”。在我看来更像是慢了 3.7%。 点是 3.7% != 数量级(我不认为这意味着你认为它意味着什么 - goo.gl/3JNvF)也许他漏掉了一个小数点【参考方案2】:只是想补充一点,任何认为 foreach 被翻译成 for 并因此没有性能差异的人是完全错误的。有很多事情发生在幕后,即不在简单的 for 循环中的对象类型的枚举。它看起来更像一个迭代器循环:
Iterator iter = o.getIterator();
while (iter.hasNext())
obj value = iter.next();
...do something
这与简单的 for 循环有很大不同。如果你不明白为什么,然后查找 vtables。此外,谁知道 hasNext 函数中有什么?据我们所知,它可能是:
while (!haveiwastedtheprogramstimeenough)
now advance
除了夸张之外,还有一些未知实现和效率的函数被调用。由于编译器不会跨函数边界进行优化,因此这里没有优化,只是您的简单 vtable 查找和函数调用。这不仅仅是理论,在实践中,通过在标准 C# ArrayList 上从 foreach 切换到 for,我已经看到了显着的加速。公平地说,它是一个包含大约 30,000 个项目的数组列表,但仍然如此。
【讨论】:
你的例子是用什么语言写的?它看起来不像是有效的 C# 或 Java。 在任何理智的集合中,不会有任何性能差异。对于链表,foreach 会更快。 对于那些不知道...或者挑剔的人来说,这就是 java。数组或数组列表是“理智”的集合吗?没有迭代器,数组运行得更快的原因是你可以直接索引项目,这意味着所有的迭代代码都是浪费的。它可能看起来很小,但想象一下当你重复 100 万次时。或者想象一下,正如之前有人指出的那样,迭代器返回对象的克隆。可能会出现严重的减速。最后,由于链表不是直接索引的,迭代代码的成本相对较小,但仍然非零! 这根本不是吹毛求疵。 Microsoft C# 编译器在看到“foreach”语句时使用基于模式的优化,其中一种优化是将数组上的“foreach”语句转换为 for(...) 语句。它甚至在某些情况下删除了边界检查以获得额外的性能。基于模式的优化在 CLR 的前通用版本中至关重要,以尝试减轻标准集合中的装箱和拆箱值类型。【参考方案3】:如果您将 C# 作为一种语言,您可能还应该考虑使用 LINQ,因为这是执行循环的另一种合乎逻辑的方式。
对列表中的每个项目执行操作您是指在列表中就地修改它,还是简单地对项目做一些事情(例如打印它、累积它、修改它等.)?我怀疑是后者,因为 C# 中的 foreach
不允许您修改正在循环的集合,或者至少不是以方便的方式...
这里有两个简单的构造,首先使用for
,然后使用foreach
,它们访问列表中的所有字符串并将它们转换为大写字符串:
List<string> list = ...;
List<string> uppercase = new List<string> ();
for (int i = 0; i < list.Count; i++)
string name = list[i];
uppercase.Add (name.ToUpper ());
(请注意,使用结束条件 i < list.Count
而不是 i < length
和一些预计算机 length
常量在 .NET 中被认为是一种好的做法,因为编译器无论如何都必须在 list[i]
时检查上限在循环中调用;如果我的理解是正确的,编译器可以在某些情况下优化掉它通常会完成的上限检查)。
这是foreach
等效项:
List<string> list = ...;
List<string> uppercase = new List<string> ();
foreach (name in list)
uppercase.Add (name.ToUpper ());
注意:基本上,foreach
构造可以迭代 C# 中的任何 IEnumerable
或 IEnumerable<T>
,而不仅仅是数组或列表。因此,集合中元素的数量可能事先不知道,甚至可能是无限的(在这种情况下,您当然必须在循环中包含一些终止条件,否则它不会退出)。
这里有一些我能想到的等效解决方案,使用 C# LINQ 表达(并且引入了 lambda 表达式 的概念,基本上是一个内联函数,采用 x
并返回 x.ToUpper ()
在以下示例中):
List<string> list = ...;
List<string> uppercase = new List<string> ();
uppercase.AddRange (list.Select (x => x.ToUpper ()));
或者使用由其构造函数填充的uppercase
列表:
List<string> list = ...;
List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ()));
或者同样使用ToList
函数:
List<string> list = ...;
List<string> uppercase = list.Select (x => x.ToUpper ()).ToList ();
或者还是和类型推断一样:
List<string> list = ...;
var uppercase = list.Select (x => x.ToUpper ()).ToList ();
或者,如果您不介意将结果作为 IEnumerable<string>
(可枚举的字符串集合),您可以删除 ToList
:
List<string> list = ...;
var uppercase = list.Select (x => x.ToUpper ());
或者可能是另一个具有类似 C# SQL 的 from
和 select
关键字,它们完全等效:
List<string> list = ...;
var uppercase = from name in list
select name => name.ToUpper ();
LINQ 的表现力很强,很多时候,我觉得代码比普通的循环更具可读性。
您的第二个问题,在列表中搜索一个项目,并希望在找到该项目时退出也可以使用 LINQ 非常方便地实现。这是foreach
循环的示例:
List<string> list = ...;
string result = null;
foreach (name in list)
if (name.Contains ("Pierre"))
result = name;
break;
下面是简单的 LINQ 等价物:
List<string> list = ...;
string result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault ();
或使用查询语法:
List<string> list = ...;
var results = from name in list
where name.Contains ("Pierre")
select name;
string result = results.FirstOrDefault ();
results
枚举仅按需执行,这意味着当在其上调用FirstOrDefault
方法时,列表只会被迭代直到满足条件。 p>
我希望这能为for
或foreach
的辩论带来更多背景,至少在.NET 世界中是这样。
【讨论】:
【参考方案4】:foreach
还会通知您您正在枚举的集合是否发生变化(即您的集合中有 7 个项目...直到另一个线程上的另一个操作删除了一个,现在您只有 6 个 @_@)
【讨论】:
请注意,如果您碰巧修改了正在循环的集合,.NET 迭代器将在获取下一个元素时抛出异常。这就是你所说的保护收藏的意思吗? 是的,我就是这个意思。我想我应该把它改成“通知”,“保护”有点用词不当呵呵 @Software Monkey - 更新了,殿下。【参考方案5】:似乎涵盖了大多数项目...以下是一些关于您的具体问题的额外说明。这些是硬性规则,而不是一种或另一种风格偏好:
我希望对列表中的每个项目执行操作
在foreach
循环中,您无法更改迭代变量的值,因此如果您希望更改列表中特定项目的值,则必须使用for
。
另外值得注意的是,“酷”的方式现在是使用 LINQ;有很多资源,有兴趣可以搜索一下。
【讨论】:
你说得对,在 .NET 中新的热点是 LINQ。这个问题我想了很久,直到现在才问。 关于搜索,这绝对不是真的,这也是我 -1 的大部分原因:你仍然可以从foreach
样式循环中找到 break
。
没有人说容器被分类了;无论如何,隐式排序的容器通常都提供自己的查找功能;并且使用 for 循环的二进制搜索不是迭代。
当一个人详细谈论循环集合,然后说“我正在列表中搜索一个项目,并希望在找到该项目时退出。”,很明显一个是专门考虑线性搜索。问题是关于迭代的,所以对二分搜索的讨论根本无关紧要,即使它涉及“循环”。此外,您试图强调效率,却忽略了一个关键的前提条件(排序)。
您的意思是尽早跳出循环是搜索算法的一种效率,因为您的陈述与您引用的陈述并列。这意味着 foreach 不能提前退出。您可以通过明确说明“高效算法”的含义来澄清,即二进制搜索,而不仅仅是找到项目后退出循环的能力。【参考方案6】:
我能想到几个原因
-
你
can't mess up indexes
,同样在移动环境中,你没有编译器优化,写得很糟糕的for循环可以做几个边界检查,而每个循环只做1个。
你can't change data input size
(添加/删除元素)同时迭代它。您的代码不会那么容易刹车。如果您需要过滤或转换数据,请使用其他循环。
您可以遍历数据结构,这些数据结构不能通过索引访问,但可以被爬取。对于每个只需要您实现iterable interface
(java) 或扩展IEnumerable
(c#)。
你可以有更小的boiler plate
,例如在解析 XML 时,它是 SAX 和 StAX 之间的区别,首先需要内存中的 DOM 副本来引用元素,后者只是迭代数据(它没有那么快,但是节省内存)
请注意,如果您使用 for each 在列表中搜索项目,您很可能做错了。考虑使用hashmap
或bimap
一起跳过搜索。
假设程序员希望使用迭代器将for loop
用作for each
,则存在跳过元素的常见错误。所以在那个场景中更安全。
for ( Iterator<T> elements = input.iterator(); elements.hasNext(); )
// Inside here, nothing stops programmer from calling `element.next();`
// more then once.
【讨论】:
在 (4) 上不正确:SAX 是一个基于事件的 API,它非常有能力,并且通常是这样实现的,从读取的流中生成的事件 - 它确实 not 需要内存中的 DOM 树。【参考方案7】:对我来说有一个好处是不太容易犯错误,例如
for(int i = 0; i < maxi; i++)
for(int j = 0; j < maxj; i++)
...
更新: 这是错误发生的一种方式。我算一笔账
int sum = 0;
for(int i = 0; i < maxi; i++)
sum += a[i];
然后决定对其进行更多聚合。所以我将循环包装在另一个中。
int total = 0;
for(int i = 0; i < maxi; i++)
int sum = 0;
for(int i = 0; i < maxi; i++)
sum += a[i];
total += sum;
当然编译失败,所以我们手动编辑
int total = 0;
for(int i = 0; i < maxi; i++)
int sum = 0;
for(int j = 0; j < maxj; i++)
sum += a[i];
total += sum;
现在代码中至少有两个错误(如果我们混淆了 maxi 和 maxj ,还有更多),这些错误只会被运行时错误检测到。如果你不写测试......这是一段罕见的代码 - 这会咬别人 - 严重。
这就是为什么将内部循环提取到方法中是个好主意:
int total = 0;
for(int i = 0; i < maxi; i++)
total += totalTime(maxj);
private int totalTime(int maxi)
int sum = 0;
for(int i = 0; i < maxi; i++)
sum += a[i];
return sum;
而且它更具可读性。
【讨论】:
真正的经典。我讨厌嵌套循环。 Linq ftw!【参考方案8】:说到干净的代码,foreach 语句比 for 语句读起来快得多!
Linq(在 C# 中)可以做很多相同的事情,但是新手开发人员往往很难阅读它们!
【讨论】:
【参考方案9】:至少在 java 中不使用 foreach 的一个原因是它会创建一个迭代器对象,该对象最终会被垃圾回收。因此,如果您尝试编写避免垃圾收集的代码,最好避免使用 foreach。但是,我相信纯数组是可以的,因为它不会创建迭代器。
【讨论】:
在 .NET 领域中,编译器有时可以检测数组,并在编译时使用 for 而不是 foreach。【参考方案10】:foreach
更简单易读
对于链表之类的结构来说,效率更高
并非所有集合都支持随机访问;迭代HashSet<T>
或Dictionary<TKey, TValue>.KeysCollection
的唯一方法是foreach
。
foreach
允许您遍历由方法返回的集合,而无需额外的临时变量:
foreach(var thingy in SomeMethodCall(arguments)) ...
【讨论】:
+1 -- 指出 'foreach' 使用 iterator 而不是索引提取。 好吧,从技术上讲,“thingy”是您的临时变量 - 只是在您的循环范围内定义 =) @bitxwise:不;thingy
对应于 i
。 for
循环需要 2 个临时对象。 (澄清)
我看到你添加了“额外”,这更有意义。不管 thingy 对应于 i,两者仍然是临时变量。您的原始答案仅指定 foreach 不需要临时变量(没有“额外”)。即使如此,您也可以引用 myCollection[i],从而减轻对额外临时变量的语法需求(我不推荐它)。无论哪种方式,我们都不要进入关于语义的精英讨论。干杯。
@bitwise - 额外的变量是 myCollection。在 foreach 循环中,您不需要将方法返回值放入变量中。【参考方案11】:
如果你可以用foreach
做你需要的事情,那就使用它;如果不是 - 例如,如果您出于某种原因需要索引变量本身 - 然后使用for
。简单!
(您的两种情况同样可以使用for
或foreach
。)
【讨论】:
【参考方案12】:foreach
在所有场景[1] 中的表现都与for
相同,包括您描述的直接场景。
但是,foreach
与for
相比具有某些与性能无关的优势:
-
方便。您不需要在周围保留额外的本地
i
(除了促进循环之外没有其他用途),并且您不需要自己将当前值提取到变量中;循环构造已经解决了这个问题。
一致性。使用foreach
,您可以轻松地遍历不是数组的序列。如果您想使用for
循环一个非数组有序序列(例如地图/字典),那么您必须稍微不同地编写代码。 foreach
在它涵盖的所有情况下都是相同的。
安全。拥有权利的同时也被赋予了重大的责任。如果您一开始就不需要循环变量,为什么还要为与递增循环变量相关的错误打开机会?
正如我们所见,foreach
在大多数情况下使用“更好”。
也就是说,如果您需要 i
的值用于其他目的,或者如果您正在处理一个您知道是数组的数据结构(并且它是一个数组有一个实际的特定原因),增加的更实用的for
提供的功能将是可行的方法。
[1] “在所有场景中”实际上是指“集合对被迭代友好的所有场景”,这实际上是“大多数场景”(参见下面的 cmets)。但是,我真的认为必须设计一个涉及迭代不友好集合的迭代场景。
【讨论】:
无论 "foreach
在所有场景中的性能是否与for
相同" 是您碰巧遍历的任何集合类型的实现细节。这当然不能以任何方式保证,但如果它们两者之间存在显着的性能差异,我会感到惊讶。
从技术上讲,我认为我的意图的正确措辞是“在涉及可以在 O(n) 中迭代的序列的所有场景中”。然而,在实践中,我认为任何集合类的编写者都不会公开性能比 O(n) 更差的迭代接口;因此我选择了表达方式。
这是一个写得很好的答案,应该被赞成,因为格式清楚地表明了关键点是什么。
@MedicineMan:格式化有很大帮助,但这不是投票的理由;内容才是最重要的。
+1,但是:“提供更多实用的功能将是可行的方法。”仅当您使用该功能时。【参考方案13】:
如果你正在迭代一个实现 IEnumerable 的集合,使用 foreach 更自然,因为迭代中的下一个成员是在完成测试到达末尾的同时分配的.例如,
foreach (string day in week) /* Do something with the day ... */
比
更直接for (int i = 0; i < week.Length; i++) day = week[i]; /* Use day ... */
您还可以在您的类自己的 IEnumerable 实现中使用 for 循环。只需让您的 GetEnumerator() 实现在循环主体中使用 C# yield 关键字:
yield return my_array[i];
【讨论】:
【参考方案14】:假设您是一家餐厅的主厨,而你们都在为自助餐准备一个大煎蛋。你把一盒一打鸡蛋递给厨房的两个工作人员,然后告诉他们,字面上的意思是,他们要开裂了。
第一个设置一个碗,打开板条箱,依次抓住每个鸡蛋 - 从左到右穿过顶排,然后是底排 - 将鸡蛋靠在碗的侧面打破,然后将其倒入碗中.最终他的鸡蛋用完了。干得好。
第二个人端起一个碗,打开板条箱,然后冲出去拿了一张纸和一支笔。他在鸡蛋盒的隔间旁边写下数字 0 到 11,并在纸上写下数字 0。他看了看纸上的数字,找到标有 0 的隔间,取出鸡蛋并将其打入碗中。他又看了看纸上的 0,想着“0 + 1 = 1”,把 0 划掉,在纸上写下 1。他从隔间 1 中取出鸡蛋并将其敲碎。以此类推,直到数字 12 出现在纸上,他知道(不看!)没有更多的鸡蛋了。干得好。
你会觉得第二个家伙脑子有点乱,对吧?
使用高级语言的目的是避免必须用计算机术语来描述事物,并且能够用您自己的术语来描述它们。语言级别越高,这一点就越真实。在循环中递增计数器会分散您真正想做的事情:处理每个元素。
此外,链表类型的结构不能通过递增计数器和索引来有效处理:“索引”意味着从头开始重新计数。在 C 中,我们可以处理我们自己创建的链表,方法是使用循环“计数器”的指针并取消引用它。我们可以使用“迭代器”在现代 C++ 中(在一定程度上在 C# 和 Java 中)做到这一点,但这仍然存在间接性问题。
最后,一些语言已经足够高级了,实际上编写循环来“对列表中的每个项目执行操作”或“搜索列表中的项目”的想法是骇人听闻(就像主厨不应该告诉第一个厨房工作人员如何确保所有鸡蛋都裂开一样)。提供了设置该循环结构的函数,您可以通过高阶函数或比较值(在搜索情况下)告诉它们在循环中要做什么。 (事实上,你可以在 C++ 中做这些事情,虽然接口有些笨拙。)
【讨论】:
我已经在互联网上的其他地方使用了一段时间。 :) 我想我可以补充一点,如果第二个人决定寻找“12 号鸡蛋”然后意识到纸箱中没有这样的插槽,他可能会惊恐发作。 ;) +1 幽默 :-)。 @Karl Knechtel 我想知道 EggIndexOutOfBoundsException 和必要的 try catch 的类比是什么样的:-)。 当然,抛出异常是一种恐慌症。有些人以比其他人更可预测和更明确的方式恐慌 :) 我认为在这种情况下,捕捉到这样的异常,相当于主厨以前见过这种情况,并且有一个快速拨号的治疗师,而不愿意教家伙如何“正常”地做到这一点(或者只是解雇他)。 +1 这是对声明式编程的精彩解释。干得好! 但是 For 比 foreach 有效,这在您的示例中并非如此。【参考方案15】:正如 Stuart Golodetz 所回答的,这是一种抽象。
如果您只使用i
作为索引,而不是将i
的值用于其他目的,例如
String[] lines = getLines();
for( int i = 0 ; i < 10 ; ++i )
System.out.println( "line " + i + lines[i] ) ;
那么就不需要知道i
的当前值了,能知道就会导致出错的可能性:
Line[] pages = getPages();
for( int i = 0 ; i < 10 ; ++i )
for( int j = 0 ; j < 10 ; ++i )
System.out.println( "page " + i + "line " + j + page[i].getLines()[j];
正如 Andrew Koenig 所说,“抽象是选择性的无知”;如果您不需要知道如何迭代某个集合的细节,那么找到一种忽略这些细节的方法,您将编写更健壮的代码。
【讨论】:
【参考方案16】:我能想到的两个主要原因是:
1) 它从底层容器类型中抽象出来。这意味着,例如,当您更改容器时,您不必更改循环遍历容器中所有项目的代码——您指定的 goal 是“为容器中的每个项目”,而不是 表示。
2) 它消除了一个错误的可能性。
就对列表中的每个项目执行操作而言,直观地说:
for(Item item: lst)
op(item);
它完美地向读者表达了意图,而不是手动使用迭代器进行操作。搜索项目也是如此。
【讨论】:
+1 表示非一个错误。并祝贺您获得第二个不错的答案徽章。 它消除了忘记进行或写入错误的停止条件(关闭一个是单一的情况)。【参考方案17】:使用 foreach 的原因:
它可以防止错误潜入(例如,您在 for 循环中忘记了i++
),这可能导致循环出现故障。搞砸 for 循环的方法有很多,但搞砸 foreach 循环的方法并不多。
它看起来更干净/不那么神秘。
for
循环在某些情况下甚至是不可能的(例如,如果您有一个 IEnumerable<T>
,它不能像 IList<T>
那样被索引)。
使用for
的原因:
IEnumerable<T>
-- foreach
只对可枚举对象进行操作。
其他特殊情况;例如,如果您从一个数组复制到另一个数组,foreach
不会为您提供可用于寻址目标数组槽的索引变量。 for
是在这种情况下唯一有意义的事情。
当您使用任一循环时,您在问题中列出的两种情况实际上是相同的——在第一种情况下,您只需一直迭代到列表的末尾,在第二种情况下,您一旦找到了break;
您正在寻找的项目。
只是为了进一步解释foreach
,这个循环:
IEnumerable<Something> bar = ...;
foreach (var foo in bar)
// do stuff
是语法糖:
IEnumerable<Something> bar = ...;
IEnumerator<Something> e = bar.GetEnumerator();
try
Something foo;
while (e.MoveNext())
foo = e.Current;
// do stuff
finally
((IDisposable)e).Dispose();
【讨论】:
一些挑剔:(1) C# 编译器将数组上的foreach
优化为for
,因此没有任何性能差异 在两个选项之间。 (2) foreach
并不严格要求 IEnumerable
/IEnumerator
实现;只要该对象具有适当的GetEnumerator
方法(即返回具有适当Current
和MoveNext
成员的对象的方法),那么您就可以通过foreach
对其进行处理。
...而且,仅当 e 实际实现 IDisposable 接口时才调用 e.Dispose()
积分;然而,这种模式在实践中几乎总是与IEnumerable
和IEnumerable<T>
一起使用。而这些接口继承IDisposable
。【参考方案18】:
Java
具有您所指向的两种循环类型。您可以根据需要使用任一 for 循环变体。你的需求可以是这样的
-
您希望重新运行列表中搜索项的索引。
您想获得项目本身。
在第一种情况下,您应该使用经典的(c 风格)for 循环。但在第二种情况下,您应该使用foreach
循环。
foreach
循环也可以用于第一种情况。但在这种情况下,您需要维护自己的索引。
【讨论】:
在 Java 中,如果您想访问列表索引,也可以使用ListIterator
。以上是关于为啥我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ?的主要内容,如果未能解决你的问题,请参考以下文章