できない.dev

配列を条件で絞り込むには

配列(リスト)から条件に合う要素だけを取り出す基本形を各言語で示す。
元の配列を変えずに新しい配列を返す点と、結果が遅延評価になる言語での扱いまでを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 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)); // 5

Array.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 のような型述語を書くと、絞り込んだ結果の型まで狭められる。

うまくいかない時: TypeScript で「Object is possibly 'null'」が解消できない

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 件なのかを先に決めると選ぶ関数が決まる。

この記事は役立ちましたか?