C#でのクロージャ

クロージャがしっかりと理解しきれていなかったと自覚したのでLearningTestを書いてみた。

まずはテストから。

using System;
using System.Collections.Generic;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

namespace LearningClosure.Tests
{
    [TestFixture]
    public class ClosureTest
    {
        [Test]
        public void コレクションクロージャメソッドは要素を走査してクロージャを呼び出すべき()
        {
            List<Employee> managers = Managers(ObjectMother.CreateEmployees());

            Assert.That(managers, Has.Count(2));
            Assert.That(List.Map(managers).Property("IsManager"), Is.All.True);
        }

        private List<Employee> Managers(List<Employee> employees)
        {
            return employees.FindAll(delegate(Employee employee) { return employee.IsManager; });
        }

        [Test]
        public void クロージャはスコープ外の変数を参照できるべき()
        {
            List<Employee> highPaidEmployees = HighPaid(ObjectMother.CreateEmployees());

            Assert.That(highPaidEmployees, Has.Count(2));
            Assert.That(List.Map(highPaidEmployees).Property("Salary"), Is.All.GreaterThan(150));
        }

        private List<Employee> HighPaid(List<Employee> employees)
        {
            int threshold = 150;
            return employees.FindAll(delegate(Employee employee) { return employee.Salary > threshold; });
        }

        [Test]
        public void クロージャを返すメソッドでもスコープ外の変数を参照できるべき()
        {
            Predicate<Employee> highPaid = PaidMore(150);
            Employee tsune = new Employee();
            tsune.Salary = 200;

            Assert.That(highPaid(tsune), Is.True);
        }

        private Predicate<Employee> PaidMore(int amount)
        {
            return delegate(Employee employee) { return employee.Salary > amount; };
        }
    }
}


次にシンプルなEmployeeクラス

using System;

namespace LearningClosure.Tests
{
    public class Employee
    {
        private int salary_;
        private bool isManager_;

        public int Salary
        {
            get { return salary_; }
            set { salary_ = value; }
        }

        public bool IsManager
        {
            get { return isManager_; }
            set { isManager_ = value; }
        }
    }
}

最後はObjectMother

using System;
using System.Collections.Generic;

namespace LearningClosure.Tests
{
    public static class ObjectMother
    {
        public static List<Employee> CreateEmployees()
        {
            List<Employee> employees = new List<Employee>();

            Employee tsune = new Employee();
            tsune.Salary = 200;
            tsune.IsManager = true;
            employees.Add(tsune);

            Employee takakuro = new Employee();
            takakuro.Salary = 200;
            takakuro.IsManager = true;
            employees.Add(takakuro);

            Employee maru = new Employee();
            maru.Salary = 100;
            employees.Add(maru);

            Employee che = new Employee();
            che.Salary = 100;
            employees.Add(che);

            return employees;
        }
    }
}

このLearningTestを書いている間に、ListクラスにはSystem.Converterデリゲートをパラメータに取るConvertAllメソッド、System.Predicateデリゲートをパラメータに取るExists, Find, FindAll, FindIndex, FindLast, FindLastIndex, Sort, RemoveAll, TrueForAllメソッド、System.Actionデリゲートをパラメータに取るForEachメソッドなど、豊富なコレクションクロージャメソッドがあることを再確認した。

このあたりについては、菊池さんの以下の記事を参考にされたい。