【C#学習記】C#機能の継承、カプセル化、ポリモーフィズム

ここに画像の説明を挿入します


論理的には、継承とカプセル化ポリモーフィズムは、学習ノートで最初に学習する必要があります。ただし、このシリーズは初心者向けではなく、継承、カプセル化、ポリモーフィズムの基本概念を習得する必要があります。この記事では、C# の継承とカプセル化ポリモーフィズムの機能のいくつかについて説明する必要があります。

C#学習メモ(2)~カプセル化、継承、ポリモーフィズムより一部抜粋

カプセル化

カプセル化とは、クラスの属性とメソッドを閉じることです。外部メンバーは直接呼び出すことができず、予約されたインターフェイスを通じてのみアクセスできます。

アクセス修飾子


以前にオブジェクトを参照してクラスを紹介したときに、アクセス修飾子が主に次の 4 つのタイプに分類されることをすでに紹介しました。

  • public: 同じアセンブリ内の他のコード、またはこのアセンブリを参照する他のアセンブリは、型またはメンバーにアクセスできます。(パブリックプロパティは継承可能)
  • private: 同じクラスまたは構造体のコードのみが型またはメンバーにアクセスできます。(プライベートプロパティは継承できません)
  • protected: 同じクラスまたはこのクラスから派生したクラス内のコードのみが型またはメンバーにアクセスできます。(このクラスとそのサブクラスにのみアクセス可能)
  • 内部: 同じアセンブリ内のコードは型またはメンバーにアクセスできますが、他のアセンブリ内のコードはアクセスできません。つまり、内部型またはメンバーは、同じコンパイルに属するコードからアクセスできます。

アクセス修飾子を使用してクラスのアクセス タイプを定義できます。
111
デフォルトでは、クラスがアクセス修飾子を定義していない場合、アクセス レベルは内部です。メンバーのアクセス修飾子が定義されていない場合、デフォルトではプライベートではありません。

class Manager // 在命名空间中定义,默认为internal,可被同一程序集的另一个代码文件访问
{
    
    
    class Parent // 在类中定义的成员类,默认为private,不可被同一程序集的另一个代码文件访问
    {
    
    }
}

(名前空間では、クラスのアクセス修飾子は、internal または public のみであることに注意してください。名前空間では他のアクセス修飾子にアクセスできないため、定義する必要はありません。)

静的クラスと静的メソッド

static 修飾子を使用して定義されたクラスは、静的クラスと呼ばれます。静的クラスは基本的に非静的クラスと同じですが、静的クラスはインスタンス化できないという 1 つの違いがあります。つまり、new 演算子を使用してクラス型の変数を作成することはできません。インスタンス変数がないため、静的クラスのメンバーにはクラス名自体を使用してアクセスできます。

たとえば、次の例に示すように、MethodA という名前のパブリック静的メソッドを持つ UtilityClass という名前の静的クラスがあるとします。

static class UtilityClass
{
    
    
	public static int a = 1;
	public static void MethodA() {
    
     }
}
static void main(){
    
    
	UtilityClass.MethodA();
}

静的クラスを使用すると、オブジェクトをインスタンス化する必要がなく、クラス内のパブリック メソッドを直接呼び出すことができます。その内部メンバーは静的のみにすることができます。(実際には、通常のクラスもクラス名を使用して静的メンバーにアクセスできます
。) 継承またはインスタンス化はできません 静的クラスの主な目的は、関連する静的メンバーを整理および管理するためのコンテナーを提供することです。

静的コンストラクター

静的クラスに初めてアクセスするとき、静的コンストラクターが 1 回呼び出されます。ただし、2 回目以降は起こらないので、初期化機能を果たします。

static class UtilityClass
{
    
    
    public static int a = 1;
    public static void MethodA() {
    
     }
    static UtilityClass()
    {
    
    
        Debug.Log("我是静态类的静态构造函数");
    }
}
class NormalClass
{
    
    
    public static void MethodA() {
    
     }
    static NormalClass()
    {
    
    
        Debug.Log("我是普通类的静态构造函数");
    }
    public NormalClass()
    {
    
    
        Debug.Log("我是普通类的实例构造函数");
    }
}
void Start()
{
    
    
    UtilityClass.MethodA(); // 我是静态类的静态构造函数
    NormalClass.MethodA(); // 我是普通类的静态构造函数
    UtilityClass.MethodA(); // 无
    NormalClass.MethodA(); // 无
    //UtilityClass u = new UtilityClass(); 错误,静态类无法实例化
    NormalClass n = new NormalClass(); // 我是普通类的实例构造函数
}

そして、通常のクラスを単独でインスタンス化すると、次のようになります。

void Start()
{
    
    
    NormalClass n = new NormalClass(); 
    // n.MethodA(); 错误,实例化对象无法访问静态方法
    // 先后输出两行:
    // 我是普通类的静态构造函数
    // 我是普通类的实例构造函数
}

継承する

継承とは、既存のクラスに基づいて新しいクラスを作成し、元のクラスの属性とメソッドを保持しながら、新しいコンテンツを追加することです。
 既存のクラスは基本クラスまたは親クラスと呼ばれ、継承されたクラスは派生クラスまたはサブクラスと呼ばれます。

継承原理

C# では、クラス、構造体、インターフェイスのみを継承でき、クラスは唯一の親クラスと任意の数の他のインターフェイス クラスを継承できます。構造体とインターフェイスはインターフェイス クラスのみを継承できます。

ここに画像の説明を挿入します
継承の各レベルでは、いくつかの継承可能なメソッドを次のレベルに追加できます。一部継承できないメンバーもございます。その関係については、上記のアクセス修飾子を参照してください。

クラス内のメンバにアクセスする場合は、上に向かって段階的にアクセスされ、サブクラスと親クラスに同名のメンバが存在する場合は、サブクラスのメンバからアクセスされます。

封印された修飾子

sealed 修飾子は、クラスが継承されないようにするために使用されます。

sealed class Father{
    
    
}

class Son:Father{
    
      //错误,不可被继承
}

シールされたクラスの目的は、最下位クラスの一部が継承されるのを防ぎ、オブジェクト指向の標準化、構造、セキュリティを強化することです。

リヒター置換原理

基本クラスが出現できる場所にはどこにでも、派生クラスが出現できます。継承の観点から見ると、サブクラスは親クラスに基づいて拡張され、当然親クラスよりも包括的になります。一方、リスコフ置換原則に従うには、サブクラスで定義された一部のメソッドが親クラスでも使用できる必要があります。

例を見てみましょう:

//基类
public class Mouse
{
    
    
    public void Dig()
    {
    
    
        Debug.Log("打洞");
    }
}
//派生类
public class MouseSon : Mouse
{
    
    
    public void Dig()
    {
    
    
        Debug.Log("不会打洞");
    }
}
static void main()
{
    
    
    Mouse m1 = new Mouse();
    m1.Dig(); // 打洞
    MouseSon m2 = new MouseSon();
    m2.Dig(); // 不会打洞,与父类方法重名时优先访问该类中的方法
}

ネズミの息子はドリルで穴を開けることができず、これはリヒター置換原則に違反します。この場合、親クラスが使用可能な間はサブクラスは使用できず、サブクラスは親クラスを置き換えることはできません。

通常、リスコフ置換原則には 2 つの基本要件があります。

  1. 親クラス オブジェクトの代わりにサブクラス オブジェクトを使用できる
  2. 親クラスのコンテナにサブクラスのオブジェクトが含まれる場合、実装は1と同じです。
//基类
public class Mouse
{
    
    
    public void Dig()
    {
    
    
        Debug.Log("打洞");
    }
}
//派生类,变异老鼠儿子,可以飞
public class MouseSon : Mouse
{
    
    
	public void Fly()
	{
    
    
        Debug.Log("飞");
    }
}
static void main()
{
    
    
    Mouse m1 = new Mouse();
    m1.Dig(); // 打洞
    MouseSon m2 = new MouseSon();
    m2.Dig(); // 打洞
    m2.Fly(); // 飞
    Mouse m3 = new MouseSon();
    m3.Dig(); // 打洞
    // m3.Fly(); 老鼠爸爸不能飞
}

継承内のコンストラクター

サブクラスが親クラスを継承する場合、サブクラスでコンストラクターを定義する場合は、親クラスがコンストラクターを定義しないか、親クラスがパラメーターなしのコンストラクターを定義します。

class Parent
{
    
    
	// 父类无构造方法或定义下列无参数构造方法
    //public Parent()
    //{
    
    
        //Debug.Log("我是Parent");
    //}
}
class Son : Parent
{
    
    
    public Son()
    {
    
    
        Debug.Log("我是Son");
    }
}

ただし、パラメータ付きのコンストラクタが親クラスに定義されており、パラメータなしのコンストラクタが定義されていない場合、サブクラスで定義されているコンストラクタ(パラメータの有無に関係なく)が定義されているとエラーが報告されます。Base キーワードを使用して、返される対応するパラメーターを定義する場合を除きます。

class Parent
{
    
    
    public Parent(int i)
    {
    
    
        Debug.Log("我是Parent");
    }
}
class Son : Parent
{
    
    
    public Son(int i) : base(i)
    {
    
    
        Debug.Log("我是Son");
    }
}

一般に、サブクラスのコンストラクターと親クラスのコンストラクターのパラメーターの数は一致する必要があります。親クラス コンストラクターが少なくとも 1 つのパラメーターを受け入れる場合、対応する子クラス コンストラクターはパラメーターを受け入れて、親クラス コンストラクターを呼び出す必要があります。


ポリモーフィズム

インターフェース

インターフェイスを定義するときは、interfaceキーワードを使用して定義する必要があります。標準インターフェイスの名前付け形式では、先頭を大文字でI表す必要がありますinterfaceインターフェイスには、メソッド、プロパティ、インデクサー、およびイベントを含めることができます。定数、フィールド、ドメイン、コンストラクター、デストラクター、静的メンバーなどの他のメンバーを含めることはできません。

interface IHuman
{
    
    
    // 接口中不允许任何显式实现
    void Name();
    void Age();
    public int Make {
    
     get; set; }
    public string this[int index]
    {
    
    
        get;
        set;
    }
}

インターフェイスを継承するときは、インターフェイスのすべてのメソッドも実装する必要があります。インターフェイスは他のインターフェイスも継承できます。インターフェイスが他の複数のインターフェイスを継承する場合、このインターフェイスは派生インターフェイスと呼ばれ、他のインターフェイスは基本インターフェイスと呼ばれます。派生インターフェイスを継承するクラスは、派生インターフェイスのすべてのメソッドを実装するだけでなく、基本インターフェイスのすべてのメソッドを実装する必要もあります。

interface IBaseInterface
{
    
    
    void BaseMethod();
}
interface IEquatable : IBaseInterface
{
    
    
    bool Equals();
}
public class TestManager : IEquatable
{
    
    
    bool IEquatable.Equals()
    {
    
    
        throw new NotImplementedException();
    }
    void IBaseInterface.BaseMethod()
    {
    
    
        throw new NotImplementedException();
    }
}

インターフェースの定義は一般的に共通の動作を定義するために使用されますが、複数のクラスでこれらの動作を実装する必要があり、その動作を実装する各クラスのメソッドを書き換える必要がある場合には、インターフェースが非常に必要になります。

インターフェースのインスタンス化

学習の過程で、最初はインターフェイスはインスタンス化できないと考えていましたが、実際にはインターネット上の多くのブログは間違っています。C# のインターフェイスはインスタンス化できます。

インターフェイスを直接インスタンス化することはできません。そのメンバーは、インターフェイスを実装する任意のクラスまたは構造体によって実装されます。(MSDN)

public class SampleClass : IControl, ISurface
{
    
    
    void IControl.Paint()
    {
    
    
        System.Console.WriteLine("IControl.Paint");
    }
    void ISurface.Paint()
    {
    
    
        System.Console.WriteLine("ISurface.Paint");
    }
}

SampleClass sample = new SampleClass();
IControl control = sample;
ISurface surface = sample;

// The following lines all call the same method.
//sample.Paint(); // Compiler error.
control.Paint();  // Calls IControl.Paint on SampleClass.
surface.Paint();  // Calls ISurface.Paint on SampleClass.

// Output:
// IControl.Paint
// ISurface.Paint

上に示したように、クラスがインターフェイスを継承すると、このクラスのインスタンスを変更されたインターフェイスのインスタンスに暗黙的に変換できます。このインターフェイス インスタンスには、object基本クラスの 4 つのメソッドと独自のインターフェイスのメソッドが含まれており、インターフェイス メソッドを呼び出すと、インターフェイス インスタンスのメソッドがクラスによってオーバーライドされたメソッドを指していることがわかります。このメソッドの利点は、クラスが同じ名前のメソッドを持つインターフェイスを継承する場合、インターフェイスをインスタンス化することで、同じ名前のメソッドへの呼び出しを区別できることです。
もちろん、このインターフェイスを実装していないクラスをこのインターフェイスに明示的に変換することもできますが、これは役に立たず、実行時にエラーが報告されます。


抽象クラスと抽象メソッド

抽象クラスと抽象メソッドの定義キーワードは、abstract抽象クラスには抽象メソッドを定義し、抽象メソッドは である必要がありますpublicただし、インターフェイスのような抽象クラスはインスタンス化できず、抽象クラス内のすべてのメソッドを実装する必要があります。抽象メソッドをオーバーライドするためのキーワードは、抽象クラスは継承を目的として作成されているため変更できないということですoverridesealed

abstract class Parent
{
    
    
   protected int age = 1; 
   public Parent() {
    
     }
   abstract public void callName();
   abstract public void callAge();
}
class Child : Parent
{
    
    
   public Child():base()
   {
    
    
   }
   public override void callName()
   {
    
    
      throw new NotImplementedException();
   }
   public override void callAge()
   {
    
    
      Debug.Log(age);
   }
}

結局のところ、抽象クラスもクラスです。特別なのは、抽象クラスをインスタンス化することはできませんが、変更する必要のないプロパティや構築メソッドなどのインスタンス化特性を持つことができることですabstractサブクラスは、通常のクラスを継承したかのように使用することもできます。
抽象クラスがインターフェイスを継承する場合、インターフェイス内にメソッドを実装する必要もありますが、抽象クラスはそれを抽象メソッドの形式で実装できます。

    abstract class Parent:IHuman
    {
    
    
        protected int age = 1;
        public Parent() {
    
     }
        abstract public void callName();
        abstract public void callAge();
        public void Name()
        {
    
    
            throw new NotImplementedException();
        }
        abstract public void Age();
    }

抽象クラスは、いくつかの特定のメソッドを実装でき、特定の属性も持つことができ、抽象メソッドの実装を除いて、通常のクラスとの違いはありません。

抽象クラスとインターフェイスの類似点と相違点

同様の点: 抽象クラスとインターフェイスの両方を実装するには継承する必要があり、どちらも直接インスタンス化することはできません。それらを継承するサブクラスは抽象 (インターフェイス) メソッドを実装する必要があり、インターフェイスを継承するサブクラスも属性とインデックスを実装する必要があります。抽象クラスとの違い
など: 抽象クラスはインスタンス化できません、インターフェイスは継承されたクラスの型変換を通じて間接的にインスタンス化できます、抽象クラスは特定のメソッドを持つことができ、インターフェイスは関数ヘッダーのみを定義できます。インターネット上の多くのブログはインターフェイスに関するものです。しかし、私の考え
は、インターフェイスはインターフェイスであり、クラスはクラスであり、これら 2 つはまったく別のものです。ブログによっては「インターフェイスと抽象クラスの違いは、インターフェイスは他のクラスを継承できないが、抽象クラスは継承できるということです。インターフェイスはそもそもクラスではありません。継承できるのはインターフェイスだけです。どうやってクラスを継承するのでしょうか?」と書いている人もいます。

仮想メソッド

キーワードを使用してvirtualメソッド、プロパティ、インデクサー、またはイベントの宣言を変更し、派生クラスでオーバーライドできるようにします。(virtual修飾子は静的プロパティには使用できません)

    class Animal
    {
    
    
        public virtual void Eat()
        {
    
    
            Debug.Log("吃素");
        }
    }
    class Lion:Animal
    {
    
    
        public override void Eat()
        {
    
    
            Debug.Log("吃肉");
        }
    }
    class Sheep:Animal
    {
    
    
    }

仮想メソッドと抽象メソッドの違いは、仮想メソッドは通常のクラスであっても抽象クラスであっても、どのクラスでも使用できることです。仮想メソッドを使用すると、親クラスに仮想メソッドを定義する必要があるかどうか、サブクラスでその仮想メソッドをオーバーライドする必要があるかどうかを柔軟に決定できます。これらはすべて自分で決定するものであり、必須の要件はありません。上記の例と同様に、オーバーライドする必要がある場合はoverrideオーバーライドされた仮想メソッドを使用し、必要ない場合は直接継承してください。さらに、virtualキーワードは関数本体を宣言する必要があります。これは、キーワードをインターフェイスや抽象メソッドで使用できないことも意味します。

        Lion lion = new Lion();
        lion.Eat(); // 吃肉
        Sheep sheep = new Sheep();
        sheep.Eat(); // 吃素
        Animal Cow = new Animal();
        Cow.Eat(); // 吃素
        Animal Tiger = new Lion();
        Tiger.Eat(); // 吃肉

同名のメソッド

サブクラスでは、主に次の 2 つの状況で、同じ名前のいくつかのメソッドを使用できます。

  1. 親クラスのメソッドをオーバーライドする場合
  2. 継承したクラスとインターフェースのメソッド名定義が同じ場合

親クラスのメソッドが new によってオーバーライドされました

例えば:

    class Animal
    {
    
    
        public void Eat()
        {
    
    
            Debug.Log("吃素");
        }
    }
    class Lion:Animal
    {
    
    
        public new void Eat() // 是否使用new关键字都会覆盖,使用new来指示这是我们有意覆盖父类方法
        {
    
    
            Debug.Log("吃肉");
        }
    }
    class Sheep:Animal
    {
    
    
    }

上記の継承構造図によると、実行時にサブクラスから親クラスに向かってレベルごとにメソッドが検索され、新しいサブクラスの同名のメソッドが親クラスのメソッドを覆っている場合、サブクラスのメソッドは直接実行されます。サブクラスが定義されていない場合は、親クラスのメソッドが使用されます。

        Lion lion = new Lion();
        lion.Eat(); // 吃肉
        Sheep sheep = new Sheep();
        sheep.Eat(); // 吃素
        Animal Cow = new Animal();
        Cow.Eat(); // 吃素
        Animal Tiger = new Lion();
        Tiger.Eat(); // 吃素

同名の継承メソッド

現在、次のようなインターフェイスが存在します。

interface IEat
{
    
    
    void Eat();
}

sheepこれで、両方を継承しAnimalIEat次のように定義されたクラスができました。

    class Wolf : Animal, IEat
    {
    
    
        public void Eat()
        {
    
    
            Debug.Log("吃肉");
        }
    }
    void Start()
    {
    
    
        Wolf wolf = new Wolf();
        wolf.Eat(); // 吃肉
        IEat eat = wolf;
        eat.Eat(); // 吃肉
    }

上記の場合、 Eat() メソッドも同時に書き換えられており、Wolfクラス内のメソッドがクラス内のメソッドをEat上書きし、同時にインターフェースのメソッドも書き換えられていることが分かりました。同名のメソッドが存在する場合、それらのメソッドは同時にオーバーライドされます。そして、正確なメソッド名を指定すると、次のようになります。AnimalEatEat

    class Wolf : Animal, IEat
    {
    
    
        public void Eat()
        {
    
    
            Debug.Log("吃肉");
        }
        void IEat.Eat()
        {
    
    
            base.Eat();
        }
    }
    void Start()
    {
    
    
        Wolf wolf = new Wolf();
        wolf.Eat(); // 吃肉
        IEat eat = wolf;
        eat.Eat(); // 吃素
    }

インターフェース名を指定することで、さまざまなメソッドを正確に定義できます。


ランタイムポリモーフィズム

override注意深い読者は次のことを発見したかもしれません:仮想メソッドをオーバーライドするとき:

        Animal Tiger = new Lion();
        Tiger.Eat(); // 吃肉

元のメソッドをオーバーライドすると、次のようになります。

        Animal Tiger = new Lion();
        Tiger.Eat(); // 吃素

実行時には、実行されるメソッド (プロパティ) をステップごとに検索するだけでなく、virtualキーワードが書き換えられているかどうかもチェックされます。親クラスのコンテナーを使用してサブクラスのオブジェクトを読み込むときに、仮想メソッドがsubclass の場合、オーバーライドされたメソッドが実行されます。仮想メソッドの実行では、最初に見つかったoverriderメソッドのみが実行されます。話は簡単です。次の例を見てください。

    class Animal
    {
    
    
        public virtual void Eat()
        {
    
    
            Debug.Log("吃素");
        }
    }
    class Lion:Animal
    {
    
    
        public override void Eat()
        {
    
    
            Debug.Log("吃肉");
        }
    }
    class Tiger : Lion
    {
    
    
        public new void Eat()
        {
    
    
            Debug.Log("吃兔子");
        }
    }

    void Start()
    {
    
    
        Animal tiger = new Tiger();
        tiger.Eat(); // 吃肉
    }

上の例では、Eat()ウサギを食べる代わりに肉を食べるという実行になっていますが、レベルごとに確認すると、最初に見つかったものはウサギを食べると出力されるはずです。ただし、基本クラスは仮想メソッドAniamlであるため、Eat()レベルごとに最初に見つかったoverrideオーバーライドされたEat()メソッドが実行されるため、出力は充実しています。

    class Tiger : Lion
    {
    
    
        public override void Eat() // 如果使用override再次重写
        {
    
    
            Debug.Log("吃兔子");
        }
    }

    void Start()
    {
    
    
        Animal tiger = new Tiger();
        tiger.Eat(); // 吃兔子
        Lion tiger = new Tiger();
        tiger.Eat(); // 吃兔子
    }

再度オーバーライドするとoverride、ウサギを食べるという表示が出ますが、興味深いのは、Lion クラスのメソッドは ではないにもかかわらず、基底クラスの仮想メソッドをvirtualオーバーライドしていることです。Animal

    class Lion:Animal
    {
    
    
        public sealed override void Eat()
        {
    
    
            Debug.Log("吃肉");
        }
    }
    class Tiger : Lion
    {
    
    
        public override void Eat() // 重写报错,Eat方法已经被密封了
        {
    
    
            Debug.Log("吃兔子");
        }
    }

Tiger継承Lion後にクラスがメソッドをオーバーライドしたくない場合は、キーワードを使用して基本クラスのメソッドをシールし、継承後にオーバーライドされないようにするEat()必要がありますsealedLion


コンパイル時のポリモーフィズム

プログラムでは、異なる状況で同じメソッドを使用しやすくするために、同じ名前でパラメーターが異なるメソッドをいくつか使用する必要があります。これをオーバーロード ( ) と呼びますoverload。次に例を示します。

    void Start()
    {
    
    
        HumanEat(吃点啥呢 ?);
    }
    string HumanEat(Animal meat) {
    
     return ""; }
    int HumanEat(Animal meat, Vegetable fruit) {
    
     return 1; }
    void HumanEat(Vegetable vegetable) {
    
     }

同じ人は同じものを食べますが、人によって食べるものは異なります。ベジタリアンはベジタリアン料理のみを食べ、ほとんどの人は肉と野菜の両方を食べ、肉食動物は肉だけを食べます。場合によっては、関数の戻り値を区別する必要があることもあります。HumanEat メソッドを呼び出す必要があり、特定の区別を行う必要がある場合は、すべての状況を 1 つの関数に入れて追加の状況が発生するたびに再ロードするのではなく、同じ関数を柔軟に呼び出すことができるように、オーバーロードが最良の選択です。オリジナルの関数を書いたり、n 個のメソッドを定義したりすると、関数名を覚えるだけでも困難になります。


概要: このセクションには多くの概念がありますが、すべての知識ポイントには独自の意味があります。たとえば、インスタンス化せずに名前空間から直接呼び出すことができるツール クラスが必要な場合は、静的クラスを使用する必要があります。静的クラスの静的コンストラクターを使用すると、初期化時に一意の呼び出しが保証されます。異なる親クラスを使用する場合は、継承パターンを考慮してください。ポリモーフィズムを使用する場合、いつ、どのような種類のポリモーフィズムを使用するかを考慮します。構造定義のみを持ち、具体的な実装がなく、インスタンス化もされない基底クラスが必要な場合は、抽象クラスが必要になり、柔軟な継承可能な関数のオーバーライドが必要な場合は、インターフェイスが必要になります。複数の状況で同じメソッドを実装する必要がある場合は、それをオーバーロードする必要があります。ポリモーフィズムが必要な場合は、オーバーライド メソッドよりも仮想メソッドの方が適しています。オブジェクト指向のすべての機能は、実際の使用時に慎重に考慮され、設計時に慎重に検討され、経験に基づいてプログラムが設計される必要があります。

おすすめ

転載: blog.csdn.net/milu_ELK/article/details/132143125