TD;LR
以下のコードを使用すると、リストを破壊的に変更(上書き)することができます。
using System;
using System.Collections.Generic;
using System.Linq;
public static class DistructiveExtensions
{
public static void Distructive<T>(this List<T> collection, Func<IEnumerable<T>, IEnumerable<T>> func)
{
if (func == null) throw new ArgumentNullException(nameof(func));
var overwritten = func(collection).ToList();
collection.Clear();
collection.AddRange(overwritten);
}
}
どういうときに破壊的メソッドを使いたいか
どうしてもリストを破壊的に変更したいというときがあると思います。
例えば、こういうコードがあります。
using System;
using System.Collections.Generic;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 4, 9, 2, 3, 5, 7, 8, 1, 6, 15
// reference : 4, 9, 2, 3, 5, 7, 8, 1, 6, 15
originを参照しているリストに対してソートをしてみます。
using System;
using System.Collections.Generic;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin.Sort();
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 1, 2, 3, 4, 5, 6, 7, 8, 9, 15
// reference : 1, 2, 3, 4, 5, 6, 7, 8, 9, 15
originとreferenceはお互いに同じオブジェクトを参照している変数なので、結果も当然同じです。
10以上の要素を除外してみましょう。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin.Where(x => x < 10);
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 4, 9, 2, 3, 5, 7, 8, 1, 6, 15
// reference : 4, 9, 2, 3, 5, 7, 8, 1, 6, 15
結果が変わりません。
それもそのはず、Sortメソッドはオブジェクトに変化を与える「破壊的メソッド」であるのに対し、Whereメソッドはオブジェクトに変化を与えない「非破壊的メソッド」です。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin = origin.Where(x => x < 10).ToList();
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 4, 9, 2, 3, 5, 7, 8, 1, 6
// reference : 4, 9, 2, 3, 5, 7, 8, 1, 6, 15
Whereメソッドはフィルターした結果を返しますが、originの参照先を変更しているだけで、やはり元のオブジェクトに変更を行っていません。
解決方法
安直な解決方法
まず思いつくのがこの方法でしょう。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
var overwritten = origin.Where(x => x < 10).ToList();
origin.Clear();
origin.AddRange(overwritten);
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 4, 9, 2, 3, 5, 7, 8, 1, 6
// reference : 4, 9, 2, 3, 5, 7, 8, 1, 6
一般的にこの方法でリストの要素を破壊的に変更することができます。しかし、例えば、この後に各要素に対して「偶数なら2で割り、奇数なら3をかけて1を足す」という処理を行い、さらに「重複した要素を除外する」処理を行い、最後に「ソートする」という複雑な処理をしようとするとコードが煩雑になります。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
{
var overwritten = origin.Where(x => x < 10).ToList();
origin.Clear();
origin.AddRange(overwritten);
}
{
var overwritten = origin.Select(x => x % 2 == 0 ? x / 2 : x * 3 + 1).ToList();
origin.Clear();
origin.AddRange(overwritten);
}
{
var overwritten = origin.Distinct().ToList();
origin.Clear();
origin.AddRange(overwritten);
}
origin.Sort();
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 1, 2, 3, 4, 10, 16, 22, 28
// reference : 1, 2, 3, 4, 10, 16, 22, 28
このコードはもっと簡単に書けます。
var overwritten = origin.Where(x => x < 10)
.Select(x => x % 2 == 0 ? x / 2 : x * 3 + 1)
.Distinct()
.ToList();
origin.Clear();
origin.AddRange(overwritten);
origin.Sort();
しかし、各操作の間にConsole.WriteLineを挟んで、途中経過を見たかったら、上記のように書くしかありません。何度も使うならメソッド化して一般化したいよね、という趣旨で書きました。
拡張メソッドを使った解決方法
以下のコードを用意します。
using System;
using System.Collections.Generic;
using System.Linq;
public static class DistructiveExtensions
{
public static void Distructive<T>(this List<T> collection, Func<IEnumerable<T>, IEnumerable<T>> func)
{
if (func == null) throw new ArgumentNullException(nameof(func));
var overwritten = func(collection).ToList();
collection.Clear();
collection.AddRange(overwritten);
}
}
10以上の要素を除外してみましょう。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin.Distructive((enumerable) => enumerable.Where(x => x < 10));
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 4, 9, 2, 3, 5, 7, 8, 1, 6
// reference : 4, 9, 2, 3, 5, 7, 8, 1, 6
いい感じですね!
複雑な処理も行ってみましょう。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin.Distructive((enumerable) => enumerable.Where(x => x < 10));
origin.Distructive((enumerable) => enumerable.Select(x => x % 2 == 0 ? x / 2 : x * 3 + 1));
origin.Distructive(Enumerable.Distinct);
origin.Sort();
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 1, 2, 3, 4, 10, 16, 22, 28
// reference : 1, 2, 3, 4, 10, 16, 22, 28
簡潔に複雑な処理ができるようになりました!
使用上の注意
本記事の方法はできれば使うべきではありません
パフォーマンス上からすると、この方法は決していい方法ではありません。
あくまで最終手段として検討してください。
また、まとめられる場合は一つにまとめるべきです。
Distructiveメソッドが呼び出されるたびに、リストは一度全部消され、上書きされています。
効率が悪いです。
紹介のために冗長に書いていますが、実際には以下のように使いましょう。
using System;
using System.Collections.Generic;
using System.Linq;
var origin = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6, 15 };
var reference = origin;
origin.Distructive((enumerable) => enumerable.Where(x => x < 10)
.Select(x => x % 2 == 0 ? x / 2 : x * 3 + 1)
.Distinct());
origin.Sort();
Console.WriteLine($"{nameof(origin)} : {string.Join(", ", origin)}");
Console.WriteLine($"{nameof(reference)} : {string.Join(", ", reference)}");
// origin : 1, 2, 3, 4, 10, 16, 22, 28
// reference : 1, 2, 3, 4, 10, 16, 22, 28