C#について
C#は、Microsoft Common Language Runtime (CLR)に到達した数少ない言語の1つです。 CLR を実装する言語は、言語間の統合、例外処理、セキュリティ強化、コンポーネント構成のための簡単なモデル、デバッグおよび分析サービスなど、CLR がもたらす機能の恩恵を受けることができます。最新の CLR 言語として C# が最も広く使用されており、そのアプリケーション シナリオは、Windows デスクトップ、携帯電話、サーバー環境などの複雑で専門的な開発プロジェクトを対象としています。
C# はオブジェクト指向の強く型付けされた言語です。 C# のコンパイル時および実行時の強力な型チェックにより、最も一般的なプログラミング エラーをできるだけ早期に発見でき、位置が非常に正確になります。これにより、型に固執せず、違反が発生してから長い間追跡可能な説明不能なエラーのみを報告する言語と比較して、プログラマの時間を大幅に節約できます。ただし、多くのプログラマは意図的または非意図的にこの検出の利点を無視しており、それがこの記事で説明するいくつかの問題を引き起こします。
この記事について
この記事では、C# プログラマーが避けるべき 10 の最も一般的なプログラミング エラー、つまりトラップを紹介します。
この記事で説明するエラーは C# 環境に固有のものですが、CLR を実装する、またはフレームワーク クラス ライブラリ (FCL) を使用する他の言語にも関連します。
よくある間違い #1: 値と同じかそれ以上の参照を使用する
C++ や他の多くの言語のプログラマーは、変数に割り当てる値が単純な値であるか、既存のオブジェクトへの参照であるかを制御することに慣れています。 C# では、これはオブジェクトをインスタンス化してそれに変数を割り当てたプログラマーではなく、オブジェクトを作成したプログラマーによって決定されます。これは、初心者の C# プログラマによくある「問題」です。
扱っているオブジェクトが値型なのか参照型なのかが分からない場合は、驚くような事態に遭遇するかもしれません。例:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
ご覧のとおり、Point オブジェクトと Pen オブジェクトは同じ方法で作成されますが、新しい X 座標値が point2 に割り当てられると、point1 の値は変更されません。新しい色の値がpen2に割り当てられると、それに応じてpen1も変更されます。したがって、point1 と point2 にはそれぞれ Point オブジェクトの独自のコピーが含まれており、pen1 とpen2 は同じ Pen オブジェクトを参照していると推測できます。このテストがなければ、どうやってこの原則を知ることができるのでしょうか?
1 つの方法は、オブジェクトがどのように定義されているかを確認することです (Visual Studio では、オブジェクトの名前にカーソルを置いて F12 キーを押すことができます)
public struct Point { … } // defines a “value” type public class Pen { … } // defines a “reference” type
上に示したように、C# では、struct キーワードは値の型を定義するために使用され、class キーワードは参照型を定義するために使用されます。 C++ プログラミングのバックグラウンドを持つ人にとって、C++ と C# の間の類似したキーワードのいくつかによって混同されている場合、この動作は驚くかもしれません。
値型と参照型で異なる動作に依存したい場合、たとえばオブジェクトをパラメータとしてメソッドに渡し、このメソッド内でオブジェクトの状態を変更したい場合。正しいタイプのオブジェクトを扱っていることを確認する必要があります。
よくある間違い #2: 初期化されていない変数のデフォルト値を誤解する
C# では、値の型を null にすることはできません。定義により、値の型値は、初期化された変数の値型であっても、値を持たなければなりません。これを型のデフォルト値と呼びます。これにより、変数が初期化されていないかどうかを確認するときに、次のような予期しない結果が生じることがよくあります:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
[point 1] は空ではないのはなぜですか?答えは、point は値型であり、デフォルト値の point (0,0) と同様に、null 値が存在しないということです。これは C# における非常に単純でよくある間違いであることに気づいていない
多くの (すべてではない) 値の型には [IsEmpty] プロパティがあり、それがデフォルト値と等しいかどうかを確認できます:
Console.WriteLine(point1.IsEmpty); // True
変数が初期化されているかどうかにかかわらず、初期化されていない変数の値は、デフォルトでは null ではない型であることを確認してください。
常见错误 #3: 使用不恰当或未指定的方法比较字符串
在C#中有很多方法来比较字符串。
虽然有不少程序员使用==操作符来比较字符串,但是这种方法实际上是最不推荐使用的。主要原因是由于这种方法没有在代码中显示的指定使用哪种类型去比较字符串。
相反,在C#中判断字符串是否相等最好使用Equals方法:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
第一个Equals方法(没有comparisonType这参数)和使用==操作符的结果是一样的,但好处是,它显式的指明了比较类型。它会按顺序逐字节的去比较字符串。在很多情况下,这正是你所期望的比较类型,尤其是当比较一些通过编程设置的字符串,像文件名,环境变量,属性等。在这些情况下,只要按顺序逐字节的比较就可以了。使用不带comparisonType参数的Equals方法进行比较的唯一一点不好的地方在于那些读你程序代码的人可能不知道你的比较类型是什么。
使用带comparisonType的Equals方法去比较字符串,不仅会使你的代码更清晰,还会使你去考虑清楚要用哪种类型去比较字符串。这种方法非常值得你去使用,因为尽管在英语中,按顺序进行的比较和按语言区域进行的比较之间并没有太多的区别,但是在其他的一些语种可能会有很大的不同。如果你忽略了这种可能性,无疑是为你自己在未来的道路上挖了很多“坑”。举例来说:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
最安全的实践是总是为Equals方法提供一个comparisonType的参数。
下面是一些基本的指导原则:
当比较用户输入的字符串或者将字符串比较结果展示给用户时,使用本地化的比较(CurrentCulture 或者CurrentCultureIgnoreCase)。
当用于程序设计的比较字符串时,使用原始的比较(Ordinal 或者 OrdinalIgnoreCase)
InvariantCulture和InvariantCultureIgnoreCase一般并不使用,除非在受限的情境之下,因为原始的比较通常效率更高。如果与本地文化相关的比较是必不可少的,它应该被执行成基于当前的文化或者另一种特殊文化的比较。
此外,对Equals 方法来说,字符串也通常提供了Compare方法,可以提供字符串的相对顺序信息而不仅仅中测试是否相等。这个方法可以很好适用于<, <=, >和>= 运算符,对上述讨论同样适用。
常见误区 #4: 使用迭代式 (而不是声明式)的语句去操作集合
在C# 3.0中,LINQ的引入改变了我们以往对集合对象的查询和修改操作。从这以后,你应该用LINQ去操作集合,而不是通过迭代的方式。
一些C#的程序员甚至都不知道LINQ的存在,好在不知道的人正在逐步减少。但是还有些人误以为LINQ只用在数据库查询中,因为LINQ的关键字和SQL语句实在是太像了。
虽然数据库的查询操作是LINQ的一个非常典型的应用,但是它同样可以应用于各种可枚举的集合对象。(如:任何实现了IEnumerable接口的对象)。举例来说,如果你有一个Account类型的数组,不要写成下面这样:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
你只要这样写:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
虽然这是一个很简单的例子,在有些情况下,一个单一的LINQ语句可以轻易地替换掉你代码中一个迭代循环(或嵌套循环)里的几十条语句。更少的代码通常意味着产生Bug的机会也会更少地被引入。然而,记住,在性能方面可能要权衡一下。在性能很关键的场景,尤其是你的迭代代码能够对你的集合进行假设时,LINQ做不到,所以一定要在这两种方法之间比较一下性能。
#5常见错误:在LINQ语句之中没有考虑底层对象
对于处理抽象操纵集合任务,LINQ无疑是庞大的。无论他们是在内存的对象,数据库表,或者XML文档。在如此一个完美世界之中,你不需要知道底层对象。然而在这儿的错误是假设我们生活在一个完美世界之中。事实上,相同的LINQ语句能返回不同的结果,当在精确的相同数据上执行时,如果该数据碰巧在一个不同的格式之中。
例如,请考虑下面的语句:
decimal total=(from accout in myaccouts where accout.status==‘active" select accout .Balance).sum();
想象一下,该对象之一的账号会发生什么。状态等于“有效的”(注意大写A)?
好吧,如果myaccout是Dbset的对象。(默认设置了不同区分大小写的配置),where表达式仍会匹配该元素。然而,如果myaccout是在内存阵列之中,那么它将不匹配,因此将产生不同的总的结果。
等一会,在我们之前讨论过的字符串比较中, 我们看见 == 操作符扮演的角色就是简单的比较. 所以,为什么在这个条件下, == 表现出的是另外的一个形式呢 ?
答案是,当在LINQ语句中的基础对象都引用到SQL表中的数据(如与在这个例子中,在实体框架为DbSet的对象的情况下),该语句被转换成一个T-SQL语句。然后遵循的T-SQL的规则,而不是C#的规则,所以在上述情况下的比较结束是不区分大小写的。
一般情况下,即使LINQ是一个有益的和一致的方式来查询对象的集合,在现实中你还需要知道你的语句是否会被翻译成什么比C#的引擎或者是其他表达,来确保您的代码的行为将如预期在运行时。
常见错误 #6:对扩展方法感到困惑或者被它的形式欺骗
如同先前提到的,LINQ状态依赖于IEnumerable接口的实现对象,比如,下面的简单函数会合计帐户集合中的帐户余额:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
在上面的代码中,myAccounts参数的类型被声明为IEnumerable
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
但是Sum方法应该定义到何处?C#是强类型的语言,因此如果Sum方法的引用是无效的,C#编译器会对其报错。我们知道它必须存在,但是应该在哪里呢?此外,LINQ提供的供查询和聚集结果所有方法在哪里定义呢?
答案是Sum并不在IEnumerable接口内定义,而是一个
定义在System.Linq.Enumerable类中的static方法(叫做“extension method”)
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
可是扩展方法和其它静态方法有什么不同之处,是什么确保我们可以在其它类访问它?
扩展方法的显著特点是第一个形参前的this修饰符。这就是编译器知道它是一个扩展方法的“奥妙”。它所修饰的参数的类型(这个例子中的IEnumerable
(另外需要指出的是,定义扩展方法的IEnumerable接口和Enumerable类的名字间的相似性没什么奇怪的。这种相似性只是随意的风格选择。)
理解了这一点,我们可以看到上面介绍的sumAccounts方法能以下面的方式实现:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
事实上我们可能已经这样实现了这个方法,而不是问什么要有扩展方法。扩展方法本身只是C#的一个方便你无需继承、重新编译或者修改原始代码就可以给已存的在类型“添加”方法的方式。
扩展方法通过在文件开头添加using [namespace];引入到作用域。你需要知道你要找的扩展方法所在的名字空间。如果你知道你要找的是什么,这点很容易。
当C#编译器碰到一个对象的实例调用了一个方法,并且它在这个对象的类中找不到那个方法,它就会尝试在作用域中所有的扩展方法里找一个匹配所要求的类和方法签名的。如果找到了,它就把实例的引用当做第一个参数传给那个扩展方法,然后如果有其它参数的话,再把它们依次传入扩展方法。(如果C#编译器没有在作用域中找到相应的扩展方法,它会抛措。)
C# コンパイラーの場合、拡張メソッドは (ほとんどの場合) コードをより明確に記述し、保守が容易になる「構文糖」です。明らかに、これは使い方を知っていることを前提としています。そうでない場合、特に最初は混乱する可能性があります。
拡張メソッドの適用には利点もありますが、拡張メソッドを理解していない、または間違った理解を持つ開発者にとっては頭痛の種となり、時間を無駄にする可能性もあります。特にオンラインのサンプルコードや、すでに書かれた他のコードを見るときはそうです。このようなコードがコンパイル エラーを生成する場合 (呼び出される型で明らかに定義されていないメソッドを呼び出すため)、一般的な傾向として、そのコードが参照されているライブラリの他のバージョンに適用されるかどうか、さらには異なるライブラリに適用されるかどうかが検討されます。新しいバージョンや「失われた」とみなされるライブラリを探すのに多くの時間が費やされます。
拡張メソッドに精通している開発者でも、拡張メソッドの名前がクラスで定義されているメソッドの名前と同じで、メソッドのシグネチャにわずかな違いがある場合に、上記の間違いを犯すことがあります。 「存在しない」スペルミスを探すのに多くの時間が費やされます。
C# では、拡張メソッドの使用がますます一般的になりつつあります。拡張メソッドは、LINQ に加えて、Microsoft の他の 2 つの広く使用されているクラス ライブラリ、Unity Application Block および Web API フレームワークでも使用されており、他にも多数あります。フレームワークが新しいほど、拡張メソッドが使用される可能性が高くなります。
もちろん、独自の拡張メソッドを作成することもできます。ただし、拡張メソッドは他のインスタンス メソッドと同じように呼び出されているように見えますが、これは実際には幻想であることに注意する必要があります。実際、拡張メソッドは拡張クラスのプライベート メンバーや保護されたメンバーにアクセスできないため、従来の継承の代わりとして使用することはできません。
よくある間違い #7: 目の前のタスクに間違ったコレクション型を使用する
C# には多数のコレクション型オブジェクトが用意されていますが、以下にリストするのはそのうちのほんの一部です:
Array、ArrayList、BitArray、BitVector32、Dictionary< K,V>,HashTable,HybridDictionary,List
ただし、場合によっては選択肢が多すぎることもありますこれは十分な選択肢がないのと同じくらい悪いことであり、コレクション型についても同様です。膨大な数のオプションにより、作業をスムーズに進めることができます。ただし、ニーズに最も適したコレクション タイプを選択できるように、事前に時間をかけて検索してコレクション タイプについて学習することをお勧めします。これにより、最終的にプログラムのパフォーマンスが向上し、エラーが発生する可能性が低くなります。
操作しているものと同じ要素タイプ (文字列やビットなど) を指定するコレクションがある場合は、最初にそれを使用することを選択することをお勧めします。このようなコレクションは、対応する要素タイプが指定されている場合により効率的です。
C# でタイプ セーフティを利用するには、非ジェネリック インターフェイスを使用するよりもジェネリック インターフェイスを使用することをお勧めします。ジェネリック インターフェイスの要素の型は、オブジェクトの宣言時に指定する型ですが、非ジェネリック インターフェイスの要素はオブジェクト型です。非ジェネリック インターフェイスを使用する場合、C# コンパイラはコードの型チェックを行うことができません。同様に、ネイティブ型のコレクションを操作する場合、非ジェネリック インターフェイスを使用すると、C# がこれらの型に対してボックス化およびボックス化解除操作を頻繁に実行することになります。これは、適切な型を指定する汎用コレクションを使用する場合と比較して、パフォーマンスに大きな影響を与えます。
もう 1 つのよくある落とし穴は、コレクション型を自分で実装することです。決してやらないというわけではありません。車輪を再発明するのではなく、.NET が提供する広く使用されているコレクション型の一部を使用または拡張することで、時間を大幅に節約できます。特に、C# の C5 汎用コレクション ライブラリと CLI は、永続ツリー データ構造、ヒープ ベースの優先キュー、ハッシュ インデックス付き配列リスト、リンク リストなど、多くの追加のコレクション タイプを提供します。
よくある間違い #8: リソース解放の欠落
CLR 管理環境はガベージ コレクターの役割を果たすため、作成されたオブジェクトによって占有されているメモリを明示的に解放する必要はありません。実際、明示的に解放することもできません。 C# には、C++ の delete に対応する演算子や、C 言語の free() 関数に対応するメソッドはありません。しかし、それはすべての使用済みオブジェクトを無視できるという意味ではありません。多くのオブジェクト タイプは、他の多くのタイプのシステム リソース (ディスク ファイル、データ接続、ネットワーク ポートなど) をカプセル化します。これらのリソースを使用し続けると、システム リソースが大幅に消費され、パフォーマンスが低下し、最終的にはプログラム エラーが発生する可能性があります。
尽管所有C#的类中都定义了析构方法,但是销毁对象(C#中也叫做终结器)可能存在的问题是你不确定它们时候会被调用。他们在未来一个不确定的时间被垃圾回收器调用(一个异步的线程,此举可能引发额外的并发)。试图避免这种由垃圾回收器中GC.Collect()方法所施加的强制限制并非一种好的编程实践,因为可能在垃圾回收线程试图回收适宜回收的对象时,在不可预知的时间内致使线程阻塞。
这并意味着最好不要用终结器,显式释放资源并不会导致其中的任何一个后果。当你打开一个文件、网络端口或者数据连接时,当你不再使用这些资源时,你应该尽快的显式释放这些资源。
资源泄露几乎在所有的环境中都会引发关注。但是,C#提供了一种健壮的机制使资源的使用变得简单。如果合理利用,可以大增减少泄露出现的机率。NET framework定义了一个IDisposable接口,仅由一个Dispose()构成。任何实现IDisposable的接口的对象都会在对象生命周期结束调用Dispose()方法。调用结果明确而且决定性的释放占用的资源。
如果在一个代码段中创建并释放一个对象,却忘记调用Dispose()方法,这是不可原谅的,因为C#提供了using语句以确保无论代码以什么样的方式退出,Dispose()方法都会被调用(不管是异常,return语句,或者简单的代码段结束)。这个using和之前提到的在文件开头用来引入名字空间的一样。它有另外一个很多C#开发者都没有察觉的,完全不相关的目的,也就是确保代码退出时,对象的Dispose()方法被调用:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
在上面示例中使用using语句,你就可以确定myFile.Dispose()方法会在文件使用完之后被立即调用,不管Read()方法有没有抛异常。
常见错误 #9: 回避异常
C#在运行时也会强制进行类型检查。相对于像C++这样会给错误的类型转换赋一个随机值的语言来说,C#这可以使你更快的找到出错的位置。然而,程序员再一次无视了C#的这一特性。由于C#提供了两种类型检查的方式,一种会抛出异常,而另一种则不会,这很可能会使他们掉进这个“坑”里。有些程序员倾向于回避异常,并且认为不写 try/catch 语句可以节省一些代码。
例如,下面演示了C#中进行显示类型转换的两种不同的方式:
// 方法 1: // 如果 account 不能转换成 SavingAccount 会抛出异常 SavingsAccount savingsAccount = (SavingsAccount)account; // 方法 2: // 如果不能转换,则不会抛出异常,相反,它会返回 null SavingsAccount savingsAccount = account as SavingsAccount;
很明显,如果不对方法2返回的结果进行判断的话,最终很可能会产生一个 NullReferenceException 的异常,这可能会出现在稍晚些的时候,这使得问题更难追踪。对比来说,方法1会立即抛出一个 InvalidCastExceptionmaking,这样,问题的根源就很明显了。
此外,即使你知道要对方法2的返回值进行判断,如果你发现值为空,接下来你会怎么做?在这个方法中报告错误合适吗?如果类型转换失败了你还有其他的方法去尝试吗?如果没有的话,那么抛出这个异常是唯一正确的选择,并且异常的抛出点离其发生点越近越好。
下面的例子演示了其他一组常见的方法,一种会抛出异常,而另一种则不会:
int.Parse(); // 如果参数无法解析会抛出异常 int.TryParse(); // 返回bool值表示解析是否成功 IEnumerable.First(); // 如果序列为空,则抛出异常 IEnumerable.FirstOrDefault(); // 如果序列为空则返回 null 或默认值
有些程序员认为“异常有害”,所以他们自然而然的认为不抛出异常的程序显得更加“高大上”。虽然在某些情况下,这种观点是正确的,但是这种观点并不适用于所有的情况。
举个具体的例子,某些情况下当异常产生时,你有另一个可选的措施(如,默认值),那么,选用不抛出异常的方法是一个比较好的选择。在这种情况下,你最好像下面这样写:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
而不是这样:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
但是,这并不说明 TryParse 方法更好。某些情况下适合,某些情况下则不适合。这就是为什么有两种方法供我们选择了。根据你的具体情况选择合适的方法,并记住,作为一个开发者,异常是完全可以成为你的朋友的。
常见错误 #10: 累积编译器警告而不处理
这个错误并不是C#所特有的,但是在C#中这种情况却比较多,尤其是从C#编译器弃用了严格的类型检查之后。
警告的出现是有原因的。所有C#的编译错误都表明你的代码有缺陷,同样,一些警告也是这样。这两者之间的区别在于,对于警告来说,编译器可以按照你代码的指示工作,但是,编译器发现你的代码有一点小问题,很有可能会使你的代码不能按照你的预期运行。
一个常见的例子是,你修改了你的代码,并移除了对某些变量的使用,但是,你忘了移除该变量的声明。程序可以很好的运行,但是编译器会提示有未使用的变量。程序可以很好的运行使得一些程序员不去修复警告。更有甚者,有些程序员很好的利用了Visual Studio中“错误列表”窗口的隐藏警告的功能,很容易的就把警告过滤了,以便专注于错误。不用多长时间,就会积累一堆警告,这些警告都被“惬意”的忽略了(更糟的是,隐藏掉了)。
但是,如果你忽略掉这一类的警告,类似于下面这个例子迟早会出现在你的代码中。
class Account { int myId; int Id; // 编译器已经警告过了,但是你不听 // Constructor Account(int id) { this.myId = Id; // OOPS! } }
再加上使用了编辑器的智能感知的功能,这种错误就很有可能发生。
现在,你的代码中有了一个严重的错误(但是编译器只是输出了一个警告,其原因已经解释过),这会浪费你大量的时间去查找这错误,具体情况由你的程序复杂程度决定。如果你一开始就注意到了这个警告,你只需要5秒钟就可以修改掉,从而避免这个问题。
记住,如果你仔细看的话,你会发现,C#编译器给了你很多关于你程序健壮性的有用的信息。不要忽略警告。你只需花几秒钟的时间就可以修复它们,当出现的时候就去修复它,这可以为你节省很多时间。试着为自己培养一种“洁癖”,让Visual Studio 的“错误窗口”一直显示“0错误, 0警告”,一旦出现警告就感觉不舒服,然后即刻把警告修复掉。
当然了,任何规则都有例外。所以,有些时候,虽然你的代码在编译器看来是有点问题的,但是这正是你想要的。在这种很少见的情况下,你最好使用 #pragma warning disable [warning id] 把引发警告的代码包裹起来,而且只包裹警告ID对应的代码。这会且只会压制对应的警告,所以当有新的警告产生的时候,你还是会知道的。.
总结
C#是一门强大的并且很灵活的语言,它有很多机制和语言规范来显著的提高你的生产力。和其他语言一样,如果对它能力的了解有限,这很可能会给你带来阻碍,而不是好处。正如一句谚语所说的那样“knowing enough to be dangerous”(译者注:意思是自以为已经了解足够了,可以做某事了,但其实不是)。
熟悉C#的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。