初級から中級へ:変数(III)
はじめに
ここで提示される資料は教育目的のみに使用されます。いかなる状況においても、提示された概念を学習し習得する以外の目的でアプリケーションを閲覧することは避けてください。
前回の「初級から中級へ:変数(II)」では、コード内でstatic変数を使用する方法について説明しました。このような変数を使用すると、グローバル変数の不必要な使用を回避できます。これで、主な変数の説明は終わりです。ただし、各変数に含めることができるデータの型については、この問題と依然として関連性のある疑問が残ります。変数のトピックの枠組みの中でこの側面を考慮するつもりはありません。これについては別のトピックで説明します。
しかし、ローカル変数とグローバル変数、変数を定数として宣言する方法と理由、さらにはstatic変数の使用方法についてすでに説明しましたが、このトピックについてまだ言うべきことは残っているのでしょうか。多くの人は意識していませんが、ある特定の種類の変数はしばしば定数と見なされることがあります。しかし、それでもなお、特別な種類の変数であることに変わりはありません。それが関数です。関数は特別なタイプの変数でありながら、一般的にはそのように考えられることは少ないかもしれません。
新しいトピックを始めましょう。ここでは、関数が特別な変数であることを理解します。
特殊変数:関数
関数について話すとき、基本的なプログラミング知識を持つ人が最初に思い浮かべるのは、「アプリケーション内で何らかの手続きを実行するために呼び出されるもの」というイメージでしょう。ただし、この定義は完全に適切というわけではありません。これは、関数ではなくプロシージャであるサブルーチンが存在するためです。これら2つのタイプには、概念的に若干の違いがあります。主な違いは、関数は値、より正確にはデータ型を返し、実行後にデータを返さないプロシージャとは異なるということです。ただし、どちらも引数を通じて異なる値を変更したり返したりすることができます。
この違いについては今すぐ深く考える必要はありません。ここでは、これから詳しく説明する内容の簡単な概要を示しているだけです。ただし、関数が「特別な種類の変数」であると私が述べた理由を理解するためには、この区別を知っておくことが重要です。
また、異なるプログラミング言語間での違いにも目を向けておく必要があります。なぜなら、将来的に学びたい言語によっては、ここで説明する概念が当てはまらない場合があるからです。これは、それぞれの言語が異なる方法でタスクを実行するためです。
JavaScriptやPythonなどのスクリプト言語では、関数は通常、「定数変数の一種」として実装されます。しかし、CやC++では、関数は定数変数として動作するだけでなく、引数を渡さずに関数内のstatic変数の値を変更できる変数としても機能します。この特徴は、一部の人にとって直感に反するまたは奇妙に思えるかもしれませんし、まるで不可能だと感じるかもしれません。しかし、CやC++ではポインタを使用することで、こうした挙動が可能になります。関数内のstatic変数をポインタで参照することで、関数を呼び出す外部のコードがそのstatic変数の値を変更できるのです。
この特性により、Cにより、CやC++のプログラミングは挑戦的であるとともに、非常に強力です。確かにリスクも伴いますが、その分、開発者に高い柔軟性を提供します。MQL5は、JavaScriptやPythonの関数の扱いと、CやC++の高度な機能の中間的な位置にあります。CやC++ほどの柔軟性は持たないものの、MQL5はより安全なコーディング環境を提供しています。
ここまでの理論的な説明は、やや退屈に感じるかもしれません。では、関数を「定数変数」としてどのように使用できるのか、シンプルな例を見てみましょう。最も一般的なケースとしては、値を定義する方法として関数を使うことがあります。これは、単純に変数に値を代入するのではなく、より意味のある分かりやすい名前を与える目的でおこないます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(One_Radian()); 07. } 08. //+------------------------------------------------------------------+ 09. double One_Radian() 10. { 11. return 180. / M_PI; 12. } 13. //+------------------------------------------------------------------+
コード01
コード例01には非常に単純な例があります。このコードを実行すると、以下に示す結果が表示されます。

図01
この時点で、あなたは、なぜこのようにするのか、11行目の計算を06行目に直接配置して、結果を印刷する方が簡単ではないのかあと自問しているかもしれません。確かに、そうです。しかし、もしそのように考えたのであれば、私がこの例で伝えようとしているポイントを完全には理解していないということです。ここでの目的は、計算を実行することではありません。目的は、コード全体でグローバルに使用できる定数を生成することです。もし毎回定数の値が必要になるたびに、その計算式を再度入力しなければならないとしたら、コードを改善したり修正したりする作業は膨大になります。しかし、定数を生成する意味のある名前を持つ関数の中にその処理をすべて含めておけば、そのプロセスはずっとシンプルで、速く、スムーズになり、プログラマーとしての効率も格段に向上するでしょう。
3Dプログラムを開発していて、度からラジアンへの変換をおこなう必要がある場合を考えてみましょう。これは1行のコードだけでなく、コード全体に散らばっている何百行にもわたる処理です。ここで質問です。毎回計算式を入力する方が簡単でしょうか、それともコード例01のような関数にまとめてしまう方が簡単でしょうか。
これが1つの例かもしれませんが、もしこれが簡単すぎる、または無意味だと感じたのであれば、前回の記事で得た知識を使って、もう少し複雑なものを作成してみましょう。以下のように実現できます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Counting(); 07. Print(Counting()); 08. Counting(); 09. Counting(); 10. Print(Counting()); 11. } 12. //+------------------------------------------------------------------+ 13. ulong Counting(void) 14. { 15. static ulong Tic_Tac = 0; 16. 17. Tic_Tac = Tic_Tac + 1; 18. 19. return Tic_Tac; 20. } 21. //+------------------------------------------------------------------+
コード02
コード例02を実行すると、次のような結果が得られます。

図02
言い換えれば、最初の例があまり適切に思えなかったとしても、2番目の例はどうでしょうか。というのも、特定のアプリケーションでイベントが発生した回数をカウントする必要がある場合があります。その場合、多くの人がグローバル変数を使ってイベントを追跡するでしょう。しかし、前回の記事でも述べたように、グローバル変数を使用することにはデメリットがあります。似たような目的の関数を使用することで、グローバル変数の不便さを排除し、コードの安定性とセキュリティを確保できます。同時に、ロジックをより簡単、迅速、実用的に修正できるようになります。
定義済み変数
変数と定数に関してすでに説明したトピックに加えて、MQL5には言及する必要がある他の重要な型があります。これは、言語機能を広範囲に使用するプログラムにとって不可欠だからです。
ここでは、これらの特別な種類の変数について簡単に触れておきます。これらの変数が存在し、特定のタスクに使用できることを理解することが重要です。
これらの変数の詳細については、ドキュメントの「定義済み変数」を参照できますが、ここでは特定のものに焦点を当てるつもりはありません。重要なのは、これらが「定義済み変数」と呼ばれているものの、典型的な変数ではないことを理解することです。私たちのコードでは、それらは定数として扱われます。しかし、MetaTrader 5においては、実際には変数です。コード内で定数として扱われているにもかかわらず、MQL5の特定の関数またはプロシージャを使用してこれらの変数の値を変更できる場合があります。
読者の皆さん、これを理解することは非常に重要です。これらの変数のいずれかの値を直接変更しようとすると、エラーが発生し、コードはコンパイルされません。「直接変更する」とは、以下に示す例のようなことを実行することを意味します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(_LastError); 07. _LastError = 2; 08. Print(_LastError); 09. } 10. //+------------------------------------------------------------------+
コード03
MQL5を初めて使用する場合は、これから説明する内容に十分注意してください。コード例03の6行目では、事前定義された変数_LastErrorに格納されている現在の値を出力するように端末に指示しています。これはMQL5で事前定義された変数なので、宣言する必要はありません。コンパイラはそれを自動的に認識できます。
ただし、コンパイラがコード例03の7行目に到達すると、次の画像に示すようにエラーが発生します。

図03
なぜこのようなエラーが発生するのでしょうか。その理由は、MQL5のこれらの定義済み変数はプログラミングレベルでは定数として扱われるためです。ただし、MetaTrader 5のコード実行レベルでは、これらの同じ変数は定数として認識されません。
これは、特にプログラミングを始めたばかりの方々にとっては、最初は混乱を招き理解しづらいかもしれません。しかし、定義済み変数についてさらに詳しく掘り下げる前に、読者の皆さんに理解しておいていただきたい点があります。それは、これらの変数はすべてのMQL5コードに存在するため、これらの定義済み変数と同じ名前の変数を作成しないようにするべきだということです。作成すると、プラットフォームによって課せられたセキュリティプロトコルに違反することになります。
そうは言っても、コード例03のように直接アクセスを試みるとエラーが発生しますが、MQL5言語自体はこれらの定義済み変数の一部を一般的な目的で変更する方法を提供しています。これらの変数にどの値を割り当てるべきかを指示する特定のルールはありません。ただし、可能な場合に変更する際は、正当な理由がある場合に限るべきです。これらの変数の値を、技術的に可能だからといって変更するべきではありません。もし言語自体を学んでいるのであれば、私たちがおこなっているように例外として学習目的でおこなうことは別です。
たとえば、SetUserError関数を使用すると、定義済み変数である_LastErrorに値を設定することができます。ただし、設定できる値は任意ではありません。割り当て可能な値は、MQL5自体で使用するために予約されているエラーコードのリストによって制限されています。このような制限を不満に思う方もいるかもしれませんが、必ずしもそれが不利なことだとは限りません。
実際にこれを確認するために、以下のコードを使用します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(_LastError); 07. SetUserError(2); 08. Print(_LastError); 09. } 10. //+------------------------------------------------------------------+
コード04
このコードは実際にコンパイルされ、実行できます。ただし、MetaTrader 5で実行すると、下の画像のようなものが表示されます。

図04
その奇妙な数字は何でしょうか。ちょっと待って。コード例04の7行目では、コード例03の7行目と同じ値を割り当てています。さて、コード例03は、これまで説明した理由によりコンパイルされませんでしたが、それでも、図04のこの値は予想していたものではありません。
実際、読者の皆さん、表示されている値は_LastError変数に割り当てるつもりだった値ですが、オフセットされています。これは、事前定義されたエラー値との競合を防ぐためにおこなわれます。_LastErrorに割り当てたい正確な値を表示するには、少し調整が必要です。しかし、その調整を見ていく前に、まず他の重要なことを理解しましょう。通常、多くのプログラマーは、コード内で定義済み変数の名前を直接使用することを避ける傾向があります。一般的には、定義済み変数の値を返す関数を使用する方が望ましいとされています。上記のコードを使うことはもちろん可能ですが、より一般的で推奨されるアプローチは、以下のようなコード構造を使用することです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(GetLastError()); 07. SetUserError(2); 08. Print(GetLastError()); 09. } 10. //+------------------------------------------------------------------+
コード05
コードは本質的に同じことを実行することに注意してください。ただし、多くの人にとって、コード05はコード04よりも読みやすいです。こうすることで、コードを読んでいる他のプログラマーに、定義済み変数の値にアクセスしていることを知らせることができます。前のトピックで説明したことがここでも適用されることに気づかれたでしょうか。定義済み変数にアクセスしてその後に読み取る方法は様々ですが、その値を変更する方法は常に同じです。例えば、_LastError変数から値を削除するために、上記のコードの7行目に示された手順を使用すべきではありません。その手順を使用すると、新しいエラー値が割り当てられます。_LastErrorのエラーをクリアまたは削除する正しい方法は、MQL5で提供されている別の手順を使用することです。以下をご覧ください。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(GetLastError()); 07. SetUserError(2); 08. Print(GetLastError()); 09. SetUserError(0); 10. Print(GetLastError()); 11. ResetLastError(); 12. Print(GetLastError()); 13. } 14. //+------------------------------------------------------------------+
コード06
さて、これが初めてこれに遭遇するのであれば、親愛なる読者の皆さん、よく注意してください。このコードを実行すると、結果は以下のようになります。

図05
次の点に注意してください。6行目が実行されると、図05の最初の行が出力され、_LastError変数にエラーや値がないことが示されます。7行目が実行されると、前述のように_LastError変数に値が割り当てられます。ただし、多くの初心者は9行目を使用して_LastError変数をクリアしようとします。しかし、これを実行して結果を印刷すると、ゼロ以外の値が得られます。なぜでしょうか。理由は先ほど述べた通りです。SetUserErrorを使用して_LastErrorに値を設定すると、値はオフセットされます。このため、この関数を使用して値を設定しようとすると、期待どおりの結果が得られません。ただし、ここでの目標は_LastError変数にゼロの値を設定することなので、これを行う正しい方法は11行目を使用することです。11行目の手順が実行されると、実際に_LastErrorに値0が割り当てられます。
これは、他のすべての定義済み変数についても同様に留意する必要があります。もちろん、これらの変数の値を設定するための正しい手順(ある場合)を理解するには、ドキュメントを参照できるので、それぞれについて個別に説明するつもりはありません。ただし、トピックの冒頭で示したように、これらの値を直接読み取ることは、事前定義された変数の名前自体を使用することによっておこなうことができます。
でも、ちょっと待ってください。ここで説明されている内容を理解しましたが、トピック全体で何度も言及されている値のオフセットの概念をまだ完全には理解していません。これをもっとわかりやすく説明できますか。そうすれば、ターミナルで変数の値を出力するときに奇妙な数字が表示されるのではなく、_LastError変数に割り当てた値を使用できる可能性があります。
確かに、これは正当な要求であり、徹底的な説明に値します。他の関連する概念についても触れるので、新しいトピックで取り上げましょう。
列挙と定数定義
プログラミング言語において、定義と列挙型の存在を定義することは、私の意見では最も難しい側面の一つです。読者の皆さん、誤解しないでください。どちらも広く使用されており、プログラミングに大いに役立っています。しかし、ドキュメントを見ただけでは、何が列挙型で、何が定義かを判断するのは難しいです。なぜなら、これはドキュメントにどのようなモデリングが使用されているかが明示されていない限り、知る術がないからです。
これらの概念について詳細に掘り下げる前に(後で説明します)、各値が何を表しているのかを理解することから始めましょう。さらに重要なのは、最初はあまり意味が分からないかもしれないテキストなど、コードにしばしば登場する要素を理解することです。この例は以下のコードで確認できます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return INIT_SUCCEEDED; 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 10. { 11. return rates_total; 12. }; 13. //+------------------------------------------------------------------+
コード07
6行目には、ほとんどすべてのインジケーターコードに出現するにもかかわらず、多くの人にとってあまり意味をなさないものが含まれています。読者の皆さん、これについては後ほどさらに詳しく説明します。しかし、私がこのコード(コード07)を紹介しているのは、よくあるコードで見かけるものを例示するためです。予約語「return」には変数や値を返すことが期待されていますが、ここでは文字列を使用しています。しかし、この文字列は何を意味し、なぜ他の文字列ではなくこれを使うのでしょうか。実際には、別のものを使用することも可能です。しかし、ここでは一歩ずつ進んでいきましょう。
定数、列挙子、構造体を見ると、MQL5内には多くの定数と列挙型が定義されていることがわかります。コード07の6行目にあるような定数や列挙型は、プログラミングにおいて名前が付けられています。これらのエンティティは、別名(エイリアス)として知られています。そして、はい、綴りは正確に示されている通りで、プログラミング言語ではアクセントを使用できません。目的は、私たちが覚えやすいように、またはコードをより読みやすくするための適切な表現を作り出すことです。それは、コードを書いた本人だけでなく、他のプログラマーにも役立ちます。
読者の皆さん、これをより明確にするために次のことを考えてみてください。あなたが何かをプログラムしているとしましょう。そして、コードがある操作を成功裏に実行したことを示すために、trueの値を返したいと考えています。どうやってそれを実現しますか。
そうですね、ゼロより大きい値を使用したり、trueという単語を書いたりすることもできます。理論上どちらも機能します。つまり、コード07の6行目の値は以下のように記述できます。
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" //+------------------------------------------------------------------+ int OnInit() { return true; }; //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; }; //+------------------------------------------------------------------+ void OnDeinit(const int reason) { Print(reason); }; //+------------------------------------------------------------------+
コード08
ただし、これをおこなうと、コードは失敗します。しかし、なぜ失敗するのでしょうか?おかしいです。結局、trueという値を使うことで、OnInitが正常に完了したことを示しているわけです。はい、親愛なる読者の皆さん、確かにtrueを返すことで、何かがエラーなく実行されたことを示しているのは事実です。しかし、それでもあなたのコードは失敗し、OnInitが実行された直後に即座に終了します。その理由は、エイリアスINIT_SUCCEEDEDに関連付けられた値にあります。
別の値を使ってはいけないというわけではありませんが、INIT_SUCCEEDEDは、単に基になる値のもっと読みやすい表現に過ぎないことを理解する必要があります。ドキュメントを確認すると、INIT_SUCCEEDEDは実際には0に対応していることがわかります。ブール論理では、0はfalseを表します。ですので、もしtrueを代わりに返すと、コード08は確実に失敗します。実際にはエラーはないのに、です。
重要な詳細:コード08を実行すると、実行直後にターミナルに次のメッセージが表示されます。

図06
現時点ではあまり理解できないかもしれませんが、時間が経てばきっと納得できるはずです。INIT_SUCCEEDEDの代わりに、コード08の行6で値0またはfalseを使用しても問題なく動作し、インジケーターはチャートから削除するまで実行され続けます。しかし、これはこの状況を処理する唯一の方法ではありません。次のようなアプローチを使用することもできます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return ERR_SUCCESS; 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 10. { 11. return rates_total; 12. }; 13. //+------------------------------------------------------------------+ 14. void OnDeinit(const int reason) 15. { 16. Print(reason); 17. }; 18. //+------------------------------------------------------------------+
コード09
コード09に示されているケースでは、コード08の6行目で値0またはfalseを使用した場合と同様に、インジケーターがチャートから削除されるとすぐに、以下に示す結果が得られます。

図07
コードをどのように記述しなければならないかを規定する厳密なルールはなく、すべてが正しく機能することを保証するために理解すべきガイドラインのみがあることに注意してください。ここで、あなたがアプリケーションにいかなるエラーも許さない、非常に細心の注意を払うプログラマーであると仮定しましょう。この場合、以下のコードを使用できます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. int OnInit() 05. { 06. return GetLastError(); 07. }; 08. //+------------------------------------------------------------------+ 09. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 10. { 11. return rates_total; 12. }; 13. //+------------------------------------------------------------------+ 14. void OnDeinit(const int reason) 15. { 16. Print(reason); 17. }; 18. //+------------------------------------------------------------------+
コード10
これは極端なケースです。OnInit関数の実行中にエラーが発生すると、返される値はゼロや、ゼロ値を示す前述のラベルとは異なる値になります。これは、前に説明したGetLastErrorが、_LastErrorに何らかの値が含まれていることを示すためです。ただし、エラーは、それがプログラミングした内容の一部であるかどうかに関係なく、コード内のどこででも発生する可能性があることを理解する必要があります。場合によっては、コード自体の障害ではなく、値間の相互作用によってエラーが発生することがあります。これにより、さまざまな理由により、_LastErrorがゼロ以外の値を保持する可能性があります。そのため、このタイプのエラー処理は極端であると考えられます。実際にこのアプローチを使用するコードを見かけることは稀です。しかし、実際に実験してみると、予期せぬ、時には面白いことが起こることがあることに気づくでしょう。
これをおこなうことで、成熟度が高まり、予期しない状況への対処についての理解が深まります。しかし、読者の皆さん、慎重に、そして忍耐強く進めてください。エラーをフィルタリングする方法をまだ説明していないため、ごく小さな詳細でも、アプリケーションが時々動作し、明らかな理由もなく時々動作しないのはなぜかと、完全に困惑してしまう可能性があります。
さて、この記事の最後のトピックとして、MQL5で内部的に定義された定数と列挙体を使用すると、作業にどのような影響が及ぶかを見てみましょう。前述したように、SetUserError関数を使用すると、システム変数_LastErrorに任意の値を割り当てることができます。しかし、SetUserErrorによって設定された値を正確に知りたい場合はどうすればよいでしょうか。これは実は非常に簡単です。SetUserErrorのMQL5ドキュメントでは、これを調整する方法について説明しています。本文の一部を以下に示します。
定義済み変数_LastErrorをERR_USER_ERROR_FIRST+user_errorに等しい値に設定します。
つまり、_LastErrorで見つかった値からERR_USER_ERROR_FIRSTを減算すると、SetUserErrorによって設定された正確な値を特定できます。次のコードに実装例を示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. Print(GetLastError()); 07. SetUserError(2); 08. Print(GetLastError()); 09. Print(GetLastError() - ERR_USER_ERROR_FIRST); 10. } 11. //+------------------------------------------------------------------+
コード11
コード11を実行すると、ターミナルに次の内容が表示されます。

図08
3つの値が表示されることに注意してください。ただし、ここでは2番目と3番目に注目します。この場合、エラーを示すためにSetUserErrorプロシージャによって割り当てられた値を正確に特定するために、値の修正が実行されるからです。
最終的な考察
ここで紹介した内容について少し付け加えておきます。SetUserErrorで負の値を使用すれば、MQL5で定義されたエラー範囲を利用して特別な何かを報告できると思うかもしれませんが、負の値は効果がありません。これは、SetUserErrorプロシージャが期待するデータ型に関連しています。
これらの状況については、次の記事で検討し、説明します。ここで示されているコードのうちいくつかは添付ファイルにあります。それらを使って、この記事で紹介した内容を自分で学習してください。良い学びを、そして次の記事でお会いしましょう。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/15304
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初心者からプロまでMQL5をマスターする(第4回):配列、関数、グローバルターミナル変数について
取引におけるニューラルネットワーク:時系列予測のための軽量モデル
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
新しい記事をご覧ください:基礎から中級へ:変数(III).
著者コードX
特殊変数:関数」というタイトルを読んで、この記事では「関数ポインタ」という特殊な型について説明しようと思った。
これについては、この目的のために作成した別の記事で後述する。なぜ関数へのポインタを使うのかを理解するためには、まず別の種類の概念、つまりイベントの扱い方を理解する必要があるからです ᙂ👍。