D初心者向け、パート1

こんにちは、Habr!



すでにかなり大人の言語であり、ネットワークはロシア語ではほとんど資料ではありません。 ギャップを埋める必要があります。 この記事では、やや退屈ですが、修飾子、属性などの非常に重要なトピックについてお話したいと思います。 Dの彼らの豊富さは、言語に精通し始めたばかりの人々を怖がらせることができます。 また、言語を使用するすべての人が完全に理解しているわけではありません。 しかし、すべてがそれほど怖いわけではなく、他の人ほど複雑ではありません)



変数の宣言と初期化



簡単なものから始めましょう:

int z; // z == int.init == 0 int a = 5; //    auto b = 5; // int auto bl = 5_000_000_000; // long,    int   auto bu = 5U; // uint, u  U ,   unsigned auto bl2 = 5L; // long,    L, l ( L)    1 auto bul = 5UL; // ulong,   U  L    const c = 5; // const(int) immutable d = 5; // immutable(int) shared e = 5; // shared(int) shared const f = 5; // shared(const(int)) shared immutable g = 5; // immutable(int) auto k; // :       ,         import std.variant; Variant k2; //   ,    ,     
      
      





明示的に、型はz、a、k2に対してのみ示され、残りのすべてではリテラルから推測されます。 変数の型を簡単に計算するために常に使用できます。 ここで主要なデータ型について読むことができます 。 リテラルに加えて、関数の結果が書き込まれた場合、変数のタイプは自動的に計算されます。

デフォルトでは、Dでは、すべての変数はストリームに対してローカルです( TLS )。別のストリームで変数を使用するには、変数を共有または不変にする必要があります。 不変がconstとどのように異なるかを説明する価値があります。 変数を作成する場合、大きな違いはなく、初期化後に変更することはできません。 関数とメソッドに渡すと大きな違いが現れるため、関数の引数を検討するときにこの問題に戻ります。



配列型



 int[] a; //   int[3] b; //   int[int] c; //   (    ) int[][] d; //   int[int[]] e; //      
      
      





後者のオプションは可能ですが、値をキーとして設定するには不変値の配列(不変)を使用する必要があるため、便利ではありません。

 e[cast(immutable(int)[])[8,3]] = 42;
      
      





そして、配列型の修飾子のトピックにスムーズに触れました

 immutable(int)[] a = [3,4]; //   int' a = [ 1, 2, 3, 4 ]; //        a,        (,  ) a.length = 8; //    a ~= a; //     a a[0] = 3; // :      ,  immutable(int) immutable(int[]) b = [8,3]; //    int,      immutable int[] c = [1,2,3]; // immutable(int[]),   
      
      





可変データの不変配列を作成する方法が見つかりませんでした。



修飾子は組み合わせることができます:

 const(shared(int)[]) a = [1]; //      shared(const(int)[]) b = [2]; //      const(shared int[]) c = [3]; //    shared(const int[]) d = [4]; //   
      
      





最初は、それらの間に大きな違いはないように見えるかもしれません。

すぐに確認することもできます
 void main() { void fnc_a( const(shared(int)[]) a ) {} void fnc_b( shared(const(int)[]) a ) {} void fnc_c( const(shared int[]) a ) {} void fnc_d( shared(const int[]) a ) {} const(shared(int)[]) a = [1]; shared(const(int)[]) b = [2]; const(shared int[]) c = [3]; shared(const int[]) d = [4]; fnc_a( a ); fnc_a( b ); fnc_a( c ); fnc_a( d ); fnc_b( a ); fnc_b( b ); fnc_b( c ); fnc_b( d ); fnc_c( a ); fnc_c( b ); fnc_c( c ); fnc_c( d ); fnc_d( a ); fnc_d( b ); fnc_d( c ); fnc_d( d ); }
      
      







最後の2つの間では、間違いなく(これは1つのタイプです)。 しかし、残りは異なります。 これについては、関数の引数に関するセクションで説明します(スポイラー「参照による配列の受け渡し」)。



string、wstring、dstringは、対応する文字の不変配列の単なるエイリアスであることに注意してください。



ポインタ



警告! C / C ++以外の動作があります

 const char * a; // const(char*),  ,    C/C++ const(char)* b; // const(char)*,  const char *  C/C++ const(char*) c; // const(char*),    ,  
      
      





そして、C / C ++で実行できるように、言語には非定数メモリへの定数ポインタを作成する構造がないことに注意する価値があります

 char * const c; //  D  
      
      





一般に、ポインターの宣言と修飾子を配布するための規則は、配列について説明されているという点で似ています([]の代わりに*です)。



警告! C / C ++以外の動作があります

Dの*(および[])は、変数識別子ではなくデータ型を明示的に参照することを常に覚えておく必要があります。

 int* a, b; //      int
      
      





Dでは、1つの宣言で異なる型の変数を宣言する方法はありません。数値とポインターが必要な場合、これは2つの異なる宣言になります。

 int a; int* b;
      
      





関数と引数



constと不変の引数から始めましょう。

 import std.stdio; class A { int val; } void func1( const A a ) { writeln( a.val ); } void func2( immutable A a ) { writeln( a.val ); } void main() { auto a = new A; //  ,  const   immutable func1( a ); //    func2( a ); //   ,     func2    A,   immutable }
      
      





したがって、この場合の不変はconstと同じではないことがわかります。 const引数を宣言するとき、この引数が関数内で変更されないことを保証します。 不変の場合、引数が初期化後に変更されないことを保証します。 最後のステートメントは、他のスレッドで共有される不変変数の使用を許可します。これらの変数は不変であるためです(どのような状況でも)。

滑りやすい瞬間があります:クラスを構造体に置き換えた場合(したがって、変数を新しいAとしてではなく、A.initとして初期化した場合)、コードは機能します。 これは、構造、数値型、静的配列が値で渡され、クラス、動的配列および連想配列が参照で渡されるためです。 また、値を渡すと、目的の型に暗黙的にキャストできるコピーが作成されます。



値で渡される型は、参照で渡すことができます。

 import std.stdio; struct A { int val; } void func0( ref A a ) { writeln( a.val ); } void func1( ref const A a ) { writeln( a.val ); } void func2( ref immutable A a ) { writeln( a.val ); } void main() { auto a = A.init; func0( a ); func1( a ); //   ,         func2( a ); //      immutable A b; func2( b ); func1( b ); //   , immutable   const func0( b ); //   ,    }
      
      





参照による配列の受け渡し
 void main() { void fnc_a( ref const(shared(int)[]) a ) {} void fnc_b( ref shared(const(int)[]) a ) {} void fnc_c( ref const(shared int[]) a ) {} const(shared(int)[]) a = [1]; shared(const(int)[]) b = [2]; const(shared int[]) c = [3]; fnc_a( a ); //fnc_a( b ); //fnc_a( c ); //fnc_b( a ); fnc_b( b ); //fnc_b( c ); //fnc_c( a ); fnc_c( b ); fnc_c( c ); }
      
      





配列はシックポインター(D配列では配列とインデックスのサイズを格納します)であり、このポインターは関数に渡されるとコピーされ、コピーされると、通常の数値と同様に目的の型にキャストできます。 ただし、リンクを暗黙的に引用することはできなくなりました。 この例の例外は、ref const(shared int [])を引数shared(const(int)[])で受け取る関数の呼び出しですが、すべてが論理的です:shared内の要素のタイプ(const(int))、および配列自体は共有されます。共有constが受け入れられます。 基本的に、例外は、定数参照を必要とする関数に単純な引数を渡すことができることです。 しかし、不変では、もう機能しません。 ただし、共有と組み合わせて、他の組み合わせも可能です。

 void main() { void fnc_a( ref immutable(shared(int)[]) a ) {} void fnc_b( ref shared(immutable(int)[]) a ) {} void fnc_c( ref immutable(shared int[]) a ) {} immutable(shared(int)[]) a = [1]; shared(immutable(int)[]) b = [2]; immutable(shared int[]) c = [3]; fnc_a( a ); //fnc_a( b ); fnc_a( c ); //fnc_b( a ); fnc_b( b ); //fnc_b( c ); fnc_c( a ); //fnc_c( b ); fnc_c( c ); }
      
      





この場合、変数aとcの型は一致するため、不変(int [])です。 不変の修飾子は、内部のすべての組み合わせを「食べます」。



さまざまなリンクで機能する関数を作成する場合はconstが適していますが、メタプログラミングを使用せずに引数に応じて対応する型を返す場合は、inoutが適しています。

 import std.stdio; inout(int)[] func( inout(int)[] a ) { return a[2..4]; } void main() { auto a = [ 1,2,3,4,5 ]; auto af = func(a); static assert( is( typeof(af) == int[] ) ); const(int)[] b = [ 1,2,3,4,5 ]; auto bf = func(b); static assert( is( typeof(bf) == const(int)[] ) ); immutable(int)[] c = [ 1,2,3,4,5 ]; auto cf = func(c); static assert( is( typeof(cf) == immutable(int)[] ) ); }
      
      





参照によって渡された引数が終了するように動作する場合(結果を書き込むために、初期値は気にしません)、特別なoutキーワードがあります:

 struct A { int val; } void func( out A a ) { } //    void main() { auto a = A(5); assert( a.val == 5 ); func( a ); assert( a.val == 0 ); //   }
      
      





funcが呼び出されると、変数aには値A.init(データ型の初期化値)が割り当てられます。



引数が変更されないことを保証して、参照によって引数を渡すことができます。 最初は、このためにinキーワードがあるように思えるかもしれませんが、inはconstスコープの省略形なので、必要なものを口頭で示す必要があります。

 void func( ref const int v ) {}
      
      





これは、コピーのオーバーヘッドを回避するために大きな構造を転送するときに役立ちます。 ただし、このようなレコードは右辺値では機能しません。つまり、この場合、リテラルにアドレスがないため、func(5)を呼び出すことはできません(これは関数出力時に作成された構造にも適用されます)。 残念ながら、これは1つの方法でのみ回避できます-テンプレートを使用する場合:

 void func(T)( auto ref const T v ) if( is(T==int) ){}
      
      





auto ref構文を使用すると、関数をインスタンス化してリンクを受け入れ、これが不可能な場合は引数のコピーを受け入れることができます。 署名制限の構築if(is(T == int))を使用すると、内部の条件が満たされた場合のみ関数をインスタンス化できます(この場合はintを持つT型のID条件)。これは常にコンパイル時です。 実際、リンク用とコピー用に2つの異なる関数がインスタンス化されます。

良くない
戻り型の自動参照構成は、後で示すように、通常の関数でも機能します。 開発者は問題について議論しており解決策さえあります 。 これは、見た目ほど簡単で明確ではありません。



Dでは、多くの言語と同様に、引数の遅延計算(使用される場合のみ引数の計算)関数があります。

 import std.stdio; void foo( bool x, lazy string str ) { writeln( "foo call" ); if( x ) writeln( str ); } string bar() { writeln( "build string" ); return "hello habr"; } void main() { writeln( "x = false" ); foo( false, bar() ); writeln( "x = true" ); foo( true, bar() ); }
      
      





もたらすでしょう

 x = false foo call x = true foo call build string hello habr
      
      





ストレージクラスの引数の完全なリスト:



スコープについて
実際、私は彼が何をしていたのか理解できませんでした。 ドキュメントには、

スコープ-パラメーター内の参照はエスケープできません(たとえば、グローバル変数に割り当てられます)


そのようなコードの操作性によって反証されるもの:

 int* glob1; int* glob2; struct A { int val; int* ptr; } void func( scope A a ) { glob1 = &(a.val); glob2 = a.ptr; } void main() { auto val = 10; auto a = A(5,&val); func( a ); assert( &val != &(a.val) ); // a  assert( &val == glob2 ); }
      
      





何かを正しく理解していなかったのでしょうか? おそらくスコープを非推奨のキーワードにしたいので、この動作の実装を見つけたのでしょう。 たぶんそれはバグです。





当然、autoキーワードを使用して戻り値を計算できます。

 auto func( int a ) { return a * 2; }
      
      





マルチプルリターンでは、囲んでいる型が計算されます:

 auto func( int a, double b ) //   double { if( a > b ) return a; else if( b > a ) return b; else return 0UL; }
      
      





可能な場合にリンクを返したい場合は、自動参照戻りタイプを使用できます。

 class A { auto ref int foo( ref int x ) { return 3; } } class B : A { override auto ref int foo( ref int x ) { return x; } } class C : A { int k; override auto ref int foo( ref int x ) { return k; } } void main() { auto a = new A; auto b = new B; auto c = new C; int val = 10; //a.foo( val ) = 12; // :    rvalue  b.foo( val ) = 14; //   :      val assert( val == 14 ); c.foo( val ) = 16; //   :        c assert( val == 14 ); assert( ck == 16 ); }
      
      





OOPの例は、このコンテキスト以外でauto refを使用する理由がよくわからないためです。通常の関数のauto refの必要性を示す良い簡単な例があれば、喜んで追加します。



2番目の部分では、@安全、純粋、nothrowおよびその他の側面について説明します。

ここでは、重要なこと(言語の初心者にとって暗黙のこと)を忘れることができるため、コメンテーターに追加します。



UPD :ポインターについて追加



All Articles