配列を条件で絞り込むには
配列(リスト)から条件に合う要素だけを取り出す基本形を各言語で示す。
元の配列を変えずに新しい配列を返す点と、結果が遅延評価になる言語での扱いまでを扱う。
公開:
各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。
Python 実行確認済み
nums = [3, 1, 4, 1, 5, 9, 2, 6]
evens = [n for n in nums if n % 2 == 0]
print(evens) # [4, 2, 6]
# filter はイテレータを返すので、そのまま print しても中身は出ない
big = filter(lambda n: n > 4, nums)
print(big) # <filter object at 0x...>
print(list(big)) # [5, 9, 6]リスト内包表記が最も読みやすく、結果はその場でリストになる。
組み込みの filter はイテレータを返すため、表示したり複数回使ったりするなら list へ変換する必要がある。
JavaScript 実行確認済み
const nums = [3, 1, 4, 1, 5, 9, 2, 6];
const evens = nums.filter((n) => n % 2 === 0);
console.log(evens); // [ 4, 2, 6 ]
// filter は新しい配列を返すので、元の配列は変わらない
console.log(nums.length); // 8
// 見つかった最初の 1 件だけが欲しいなら find を使う
console.log(nums.find((n) => n > 4)); // 5Array.prototype.filter は条件が真になった要素だけを集めた新しい配列を返し、元の配列は変更しない。
1 件だけ欲しい場面で filter を使うと不要な走査が残るので、その場合は find を選ぶ。
TypeScript 実行確認済み
const values: (string | null)[] = ["a", null, "b"];
// 型述語を書かないと結果は (string | null)[] のままになる
const strs: string[] = values.filter((v): v is string => v !== null);
console.log(strs.map((s) => s.toUpperCase())); // [ 'A', 'B' ]
const nums: number[] = [3, 1, 4, 1, 5, 9, 2, 6];
const evens: number[] = nums.filter((n) => n % 2 === 0);
console.log(evens); // [ 4, 2, 6 ]値を絞り込んでも、コールバックが boolean を返すだけでは要素の型は変わらず null を含んだままになる。
v is string のような型述語を書くと、絞り込んだ結果の型まで狭められる。
Go 静的確認
package main
import "fmt"
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
evens := make([]int, 0, len(nums))
for _, n := range nums {
if n%2 == 0 {
evens = append(evens, n)
}
}
fmt.Println(evens) // [4 2 6]
}Go には条件で絞り込んだ新しいスライスを返す標準関数が無いので、append で積み直すのが基本形になる。
make の第 3 引数に元の長さを渡しておくと、途中でのメモリ再確保を避けられる。
Rust 静的確認
fn main() {
let nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
// copied で &i32 を i32 にしてから集める
let evens: Vec<i32> = nums.iter().copied().filter(|n| n % 2 == 0).collect();
println!("{:?}", evens); // [4, 2, 6]
}イテレータの filter は遅延評価なので、collect を呼ぶまで何も走らない。
iter は参照を返すため、値の Vec が欲しいときは copied や cloned を挟んでから集める。
Java 実行確認済み
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6);
List<Integer> evens = nums.stream().filter(n -> n % 2 == 0).toList();
System.out.println(evens); // [4, 2, 6]
// 元のリストは変わらない
System.out.println(nums.size()); // 8
}
}Stream の filter は中間操作なので、toList のような終端操作を呼んで初めて評価される。
Stream.toList は Java 16 で追加されたメソッドで、それ以前は collect(Collectors.toList()) を使う。
C# 実行確認済み
using System;
using System.Linq;
class Program {
static void Main() {
int[] nums = { 3, 1, 4, 1, 5, 9, 2, 6 };
var evens = nums.Where(n => n % 2 == 0).ToArray();
Console.WriteLine(string.Join(",", evens)); // 4,2,6
// Where は遅延評価なので、列挙するまで条件は走らない
var q = nums.Where(n => n > 4);
Console.WriteLine(q.Count()); // 3
}
}LINQ で絞り込みを担うのは Where で、Filter という名前のメソッドは無い。
戻り値は遅延評価される IEnumerable なので、結果を配列やリストとして固定したいなら ToArray や ToList を呼ぶ。
元の配列は変わらない
ここで挙げたどの書き方も、条件に合う要素を集めた新しい入れ物を返すだけで、元の配列には手を触れない。
絞り込んだつもりで元の変数を使い続けると、件数が減っていないように見える。
結果は必ず受け取って使う。
逆に元の配列そのものから取り除きたい場合は、Go の slices.DeleteFunc や Java の List.removeIf のように破壊的な API を選ぶ必要がある。
遅延評価に注意する
Python の filter、Rust のイテレータ、Java の Stream、C# の LINQ はいずれも遅延評価で、終端の操作を呼ぶまで条件は走らない。
print や Console.WriteLine にそのまま渡すとオブジェクトの表現だけが出て中身が見えず、条件が間違っているのか評価されていないのかを取り違えやすい。
確認するときは list や collect、ToArray で確定させてから見る。
つまずき(1 件だけ欲しいとき)
「条件に合う最初の 1 件」が目的なら、絞り込んでから先頭を取る書き方は無駄が多く、0 件のときの扱いも自分で書くことになる。
JavaScript は find、Python は next(iter, default)、Java は findFirst、C# は FirstOrDefault のように専用の手段があり、見つからなかった場合の戻り値も定義されている。
目的が全件なのか 1 件なのかを先に決めると選ぶ関数が決まる。