696
690

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【C#】わかった"つもり"になれる「ラムダ式」解説

Last updated at Posted at 2020-03-10

この記事について

この記事ではC#の「ラムダ式」についてなんとなくわかったつもりになれるを目標に説明をします。
そのため多少端折ってたり、厳密じゃない説明もでてきます。ご了承ください。

もし厳密な解説を知りたい方は、次の記事がオススメです。

この記事で出てくる用語

  • ラムダ式
  • デリゲート
    • Action
    • Action<T>
    • Func<TResult>
    • Func<T, TResult>

はじめに ラムダ式って何

C#に限らず、プログラミングをしていると「ラムダ式」という用語を耳にすることがあります。
果たしてこの「ラムダ式」とは何をするためのもので、あるとどうして嬉しいのか、それぞれ解説していきます。

「その場で」関数を書ける機能がラムダ式

「ラムダ式」という名前からしてかなりいかつい印象をうける人もいるでしょう。
ですがラムダ式自体はとても単純で、次の2つの機能を指しています。

  • その場にサクッと関数を定義するかんたんな関数の書き方
  • 「式木」を書くために使う記法

普段、ラムダ式といった場合は前者の「かんたん関数記法」のことを指しています。
そのため「式木」のことは忘れてもらって大丈夫です。

ちなみに、なんで「ラムダ式」って名前なのか

諸説ありますが、由来としては「計算式を書く時に同じ式を何度も書くと大変なので、対象の関数をΛ(ラムダ)と置いて記述を省略する」というところから来ています。
数学では変数をxyと置いて計算しますが、それの関数バージョンがΛだったということになります。

つまり、「ラムダ」という記号に深い意味などはなく、単に習慣で決まっただけの名前ということです。

ラムダ式の記法

たとえば次のような関数があったとします。

// 2つのint型を入力して、1つのint型を返す
private int Add(int x, int y)
{
    return x + y;
}

これをラムダ式で書くと、このような記法となります。

// ラムダ式だけを単体で書くことはなく、
// 基本はこれをデリゲート(後述)に代入することになる
(x, y) => x + y;

ラムダ式の記法は、最初は見てもよくわからないでしょう。
なぜなら、「ラムダ式は関数をサクッと手短に書くために、無駄なタイプ量を減らすために徹底的に簡略化された記法だから」です。

省略している部分を省いて、もう少し丁寧に書くこともできます。

// こう書いてもOK(しかし冗長)
(int x, int y) => { return x + y; };

こうなるとわかりやすいでしょう。元の関数Add()と比較してみましょう。

// 普通の関数記法
private int Add(int x, int y)
{
    return x + y;
}

// ラムダ式の記法
(int x, int y) => { return x + y; };

// 徹底的に省略した記法
// 関数本体が1行で済むなら{}とreturnを省略できる
(x, y) => x + y;
(引数の変数) → {関数本体}

という記法で構成されています。つまり=>は「矢印(→)」を意味しています。

元の関数記法と比べて、ラムダ式では次の点が異なります。

  • 関数名が存在しない(つけられない)
  • 引数の型が省略できる(int x、とかしなくて単にxだけで済む)
  • 返り値の型が省略できる(int Add()、という返り値の型を省略できる)

関数名がつけられない理由としては、ラムダ式は使い捨ての関数をその場でサクっと定義するために使うものだからです。
「わざわざ名前をつけて関数定義したくない」、という怠惰なプログラマのニーズに答えた結果こうなっています。
(ちなみにこの「使い捨ての関数」、正確には「匿名関数/匿名メソッド」と呼んだりします)

また型が省略できるのは「型推論」という機能がC#に備わっているためです。
つまりはコンパイラが空気を読んで型を推測してくれているからです。

引数なしのラムダ式の書き方

ちなみに、引数が空の関数をラムダ式で書く場合は次のように書きます。

// 普通の関数記法
private void Do()
{
    Console.WriteLine("Do!");
}

// ラムダ式
() => Console.WriteLine("Do!");

() =>と、空の括弧を書けば引数無しという扱いになります。

ラムダ式があると何が嬉しいのか

ラムダ式は使い捨ての関数をその場でサクっと定義するというのに向いています。

「使い捨ての関数」がどのような場面で役に立つかというと、たとえば次のコードをみてください。

N秒経ったら関数を実行する

たとえば、N秒待った後に別の関数を実行する場合は次のように書けます。

using System;
using System.Threading.Tasks;

namespace Samples
{
    public class SampleClass
    {
        // このメソッドが外からコールされる想定
        public void Start()
        {
            // 5秒経ったら関数を呼ぶ
            // _ は"discard"を意味(await漏れによる警告抑制)
            _ = WaitForAsync(5);
        }

        private async Task WaitForAsync(float seconds)
        {
            await Task.Delay(TimeSpan.FromSeconds(seconds));
            Do();
        }

        private void Do()
        {
            Console.WriteLine("Do!");
        }
    }
}

WaitForAsync()メソッドを呼び出すことで、N秒後にDo()関数が実行されます。

ただ、この記法には次の欠点があります。

  • WaitForAsync()メソッドの呼び出し場所と、N秒後に実行される関数定義が別の場所にあって処理が追いにくい
  • 単純な処理でもいちいち関数として事前に用意しておかないといけない

N秒後に実行して欲しい処理をその場に書きたい

これらの問題を解決するのものが「ラムダ式」です(あとデリゲート)。

using System;
using System.Threading.Tasks;

namespace Samples
{
    public class SampleClass2
    {
        public void Start()
        {
            // 5秒経ったら関数を呼ぶ
            _ = WaitForAsync(5, () => Console.WriteLine("Do!"));
        }

        private async Task WaitForAsync(float seconds, Action action)
        {
            await Task.Delay(TimeSpan.FromSeconds(seconds));
            action();
        }
    }
}

ラムダ式を使うことで「関数の呼び出し時に即席で関数を作って登録する」ということができました。

WaitForAsync(5, () => Console.WriteLine("Do!"));

このように、「いちいち関数を定義するのが面倒くさい」というニーズに答えるための省略記法がラムダ式です。

では、このラムダ式ですが、単体では使うことはできません。
必ずデリゲートとセットで扱う必要があります。

関数を代入できる変数 デリゲート

ラムダ式とセットで扱うものがデリゲートです。
いったんラムダ式のことを忘れ、デリゲートの解説をします。

デリゲートって何

デリゲートとは、「関数を代入できる変数」みたいなものです。
関数をデリゲートに登録しておいて、後からデリゲート経由でその関数を呼び出す、ということができるようになります。

デリゲートは何に使うのか

デリゲートを使うと、「登録する関数を自由に差し替えて、実行時に異なる関数を実行する」という処理が簡単に実装できることです。

// N秒後にデリゲートに登録された関数を呼び出す非同期関数
private async Task WaitForAsync(float seconds, Action action)
{
    await Task.Delay(TimeSpan.FromSeconds(seconds));
    action();
}

たとえば、さきほどのWaitForAsyncもデリゲート(Action型)を使っています。
Action型は「引数なし、返り値なし」の関数を登録することができるデリゲートです。

このように、「関数の引数をデリゲートにする」ことで、「関数から別の関数を呼び出す」という処理を外から切り替えることができるようになります。

デリゲートの記法

C#の歴史上、デリゲートにはいろんな種類が存在します。ですが今日においてはジェネリック版のデリゲート(ActionFunc)しかほぼ使いません。
そのため今回はこちらに絞って説明を行います。

Action 引数なし、返り値なし

「引数なし、返り値なし」の関数を登録できるのがAction型のデリゲートです。

private void Do()
{
    Console.WriteLine("Do!");
}

public void Start()
{
    // Action型のデリゲートに、Do関数を登録
    Action action = Do;

    // デリゲート実行(Doが呼び出される)
    action();
    
    // Invoke()を呼んでもOK(Doが呼び出される)
    action.Invoke();
}

Action<T> 引数あり、返り値なし

「引数あり、返り値なし」の関数はAction<T>型のデリゲートを使います。

private void Print(string message)
{
    Console.WriteLine(message);
}

public void Start()
{
    // string型を引数にとるデリゲート
    Action<string> action = Print;

    // Print("Hello!")が実行される
    action("Hello!");
}

なお、引数の数が増える場合はその分Action<T>Tの数が増えます。

private void Print(string message1, string message2, string message3)
{
    Console.WriteLine(message1);
    Console.WriteLine(message2);
    Console.WriteLine(message3);
}

public void Start()
{
    // string型を3つ引数にとるデリゲート
    Action<string, string, string> action = Print;

    // Print("Hello", "World", "!")が実行される
    action("Hello", "World", "!");
}

Func<TResult> 引数なし、返り値あり

Func<TResult>は「引数なし、返り値あり」の関数が登録できるデリゲートです。
Func<TResult>と書いてあった場合、「TResult型の結果が返ってくるんだな」と理解してもらえればいいです。

private int CreateRandomNumber()
{
    var random = new Random();
    return random.Next();
}

public void Start()
{
    // int型を返り値とする関数を登録できるデリゲート
    Func<int> func = CreateRandomNumber;

    // CreateRandomNumber()が実行される
    var result = func();
}

Func<T, TResult> 引数あり、返り値あり

Func<T, TResult>は「引数あり、返り値あり」の関数が登録できるデリゲートです。
T型を引数にとり、TResult型を返す関数が登録できます。

private int CreateRandomNumber(int max)
{
    var random = new Random();
    return random.Next(0, max);
}

public void Start()
{
    // int型を返り値とする関数を登録できるデリゲート
    Func<int, int> func = CreateRandomNumber;

    // CreateRandomNumber(100)が実行される
    var result = func(100);
}

なお、引数の数が増える場合は手前のTの数が増えます。
たとえば、Func<T1, T2, T3, TResult>のように。

この場合はT1型、T2型、T3型を引数にとり、TResult型を返すという意味になります。
ここで重要なのは、Funcの最後の型が返り値の型であることは固定であることです。

private double CreateRandomNumber(int min, int max)
{
    var random = new Random();
    var v = random.NextDouble(); // 0.0 - 1.0
    return (max - min) * v + min;
}

public void Start()
{
    // int型2つを引数にとり、double型を返す関数を登録できる
    Func<int, int, double> func = CreateRandomNumber;

    // CreateRandomNumber(0, 100)が実行される
    var result = func(0, 100);
}

ラムダ式とデリゲート

ラムダ式で作った関数は必ずデリゲートに登録される

ラムダ式を単体で使うことはなく、ラムダ式で生成した関数は必ずデリゲートに代入して使います

private int Add(int x, int y)
{
    return x + y;
}

public void Start()
{
    // 関数をデリゲートに登録
    Func<int, int, int> func1 = Add;

    // ラムダ式で作った関数を登録
    Func<int, int, int> func2 = (x, y) => x + y;

    // 呼び出し
    Console.WriteLine(func1(10, 20));
    Console.WriteLine(func2(10, 20));
}

関数の引数をデリゲートにしておけば、ラムダ式で楽ができる

関数の引数として「デリゲート」を受けとれるようにおけば、ラムダ式を使って「その場で使い捨て関数を作って登録」ということができるようになります。

using System;
using System.Threading.Tasks;

namespace Sample
{
    public class Sample3 : IDisposable
    {
        private void Start()
        {
            // 5秒経ったらログを出す
            _ = WaitForAsync(5, () => Console.WriteLine("5!"));

            // 10秒経ったら別のログを出す
            _ = WaitForAsync(10, () => Console.WriteLine("10!"));

            // 30秒経ったらログを出して削除
            _ = WaitForAsync(30, () =>
            {
                Console.WriteLine("Bye!");
                Dispose();
            });
        }

        private async Task WaitForAsync(float seconds, Action action)
        {
            await Task.Delay(TimeSpan.FromSeconds(seconds));
            action();
        }

        public void Dispose()
        {
            Console.WriteLine("Disposed");
        }
    }
}

LINQも実は引数はデリゲート

便利なLINQですが、実はこれも引数はデリゲートになっています。
たとえば、よく使うSelect()ですが、これの引数は実はFunc<T, TResult>になっています。

var array = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 整数配列のそれぞれの逆数の総和を計算する
var reciprocalSum = array
    .Select(x => 1.0f / x)
    .Sum();

Console.WriteLine(reciprocalSum);

1.png

(型推論が効いて今はFunc<int, float>が適用されているとIDEに表示されている)

別にラムダ式はLINQ専用のためのものではなく、単にLINQがデリゲートを使っているからラムダ式がうまく利用できて便利だったという話です。

補足: デリゲートなので実関数の登録もできる

LINQはただのデリゲートを使っているだけなので、ラムダ式を使わずに普通の関数を登録することもできます。

public void Start()
{
    var array = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};

    var reciprocalSum = array
         // Func<int, float> に Reciprocal()を登録
        .Select(Reciprocal)
        .Sum();

    Console.WriteLine(reciprocalSum);
}

// 逆数を返す関数
private float Reciprocal(int x)
{
    return 1.0f / x;
}

LINQのコードを書くたびに、ちょっとした関数を毎回定義していたのでは面倒くさすぎます。
それに処理が散らかるため可読性も悪くなってしまいます。

このように、ラムダ式は「使い捨ての関数をその場で定義できる」という、とても画期的な機能ということでした。

まとめ

  • 「ラムダ式」はその場で使い捨ての関数を定義できる便利機能
  • 「ラムダ式」という名前はいかついけど、数学で変数をxとする、くらいの意味と同じでしかない
  • 「ラムダ式」は 「デリゲート」と組み合わせて使う
  • LINQRxは単にデリゲートを使っているだけで、そこにラムダ式が便利に当てはめられているだけ
  • 自分でデリゲートを定義して使うこともできる

あと、たまに「ラムダ式を代入する」という表現を使う人がいるのですが、厳密には間違っています。
正確には「ラムダ式で生成された関数(匿名関数)をデリゲートに代入する」です。

696
690
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
696
690

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?