できない.dev

配列に要素が含まれるか調べるには

配列(リスト)に特定の要素が含まれるかを調べる基本形を各言語で示す。
JavaScript の in 演算子の落とし穴、NaN やオブジェクトの比較、条件で探す書き方までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

fruits = ["apple", "banana", "cherry"]
print("banana" in fruits)      # True
print("grape" not in fruits)   # True
 
# 位置も欲しいなら index。見つからないと ValueError になる
print(fruits.index("banana"))  # 1
 
# 比較は == で行われるので、中身が同じリストも見つかる
print([1, 2] in [[1, 2], [3, 4]])  # True
 
# NaN は自分自身と等しくないが、in は先に同一性(is)を見る
nan = float("nan")
print(nan in [nan])            # True(同じオブジェクト)
print(float("nan") in [nan])   # False(別のオブジェクト)

in 演算子は要素を先頭から == で順に比べるため、中身が同じリストでも見つかる。
位置が必要なときは index を使うが、無い値を渡すと ValueError になるので、先に in で確認するか例外を処理する。

JavaScript 実行確認済み

const fruits = ["apple", "banana", "cherry"];
console.log(fruits.includes("banana")); // true
console.log(fruits.indexOf("grape"));   // -1(見つからないときは -1)
 
// NaN は includes なら見つかるが、indexOf では見つからない
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN));  // -1
 
// オブジェクトは参照で比較されるので、中身が同じでも見つからない
console.log([{ id: 1 }].includes({ id: 1 }));     // false
console.log([{ id: 1 }].some((u) => u.id === 1)); // true
 
// in 演算子が調べるのは要素ではなくインデックス
console.log("banana" in fruits); // false
console.log(1 in fruits);        // true

includes は真偽値を返し、NaN も見つけられる。
indexOf は === で比較するため NaN を見つけられず、見つからないときは -1 を返す。
オブジェクトの配列は参照で比べられるので、中身で探すときは some に条件を書く。

TypeScript 実行確認済み

const fruits: string[] = ["apple", "banana", "cherry"];
const input: string = "banana";
console.log(fruits.includes(input)); // true
 
// as const の配列は要素がリテラル型になるため、string を渡す includes は型エラーになる
const ROLES = ["admin", "editor"] as const;
const role: string = "admin";
// console.log(ROLES.includes(role)); // error TS2345
console.log((ROLES as readonly string[]).includes(role)); // true
 
// 型ガードにしておけば、通過後の値は "admin" | "editor" に絞られる
function isRole(value: string): value is (typeof ROLES)[number] {
  return (ROLES as readonly string[]).includes(value);
}
console.log(isRole("editor"), isRole("guest")); // true false

型の上では string[] の includes は string を受け取る。
ただし as const で作った読み取り専用のリテラル配列では引数もリテラル型の union に絞られるため、string 型の値を渡すとコンパイルエラーになる。
readonly string[] として扱い直すか、型ガード関数にまとめると、通過後の値の型も絞れる。

うまくいかない時: TypeScript で「Argument of type ... is not assignable」が解消できない

Go 実行確認済み

package main
 
import (
	"fmt"
	"slices"
)
 
type User struct{ ID int }
 
func main() {
	fruits := []string{"apple", "banana", "cherry"}
	fmt.Println(slices.Contains(fruits, "banana")) // true
	fmt.Println(slices.Index(fruits, "grape"))     // -1
 
	// 条件で探すなら ContainsFunc(Go 1.21 以降)
	users := []User{{ID: 1}, {ID: 2}}
	fmt.Println(slices.ContainsFunc(users, func(u User) bool { return u.ID == 2 })) // true
}

Go 1.21 以降は標準の slices パッケージに Contains と Index があり、手書きのループが要らなくなった。
比較は == で行われるため要素は比較可能な型である必要があり、条件で探すときは ContainsFunc を使う。

Rust 実行確認済み

fn main() {
    let fruits = vec!["apple", "banana", "cherry"];
    println!("{}", fruits.contains(&"banana")); // true(引数は参照で渡す)
    println!("{:?}", fruits.iter().position(|&f| f == "grape")); // None
 
    // Vec<String> に &"bob" を渡すと型が合わずコンパイルエラーになる
    let names = vec![String::from("alice"), String::from("bob")];
    println!("{}", names.iter().any(|n| n == "bob")); // true
    println!("{}", names.contains(&"bob".to_string())); // true
}

スライスの contains は要素への参照を受け取るので、&"banana" のように参照で渡す。
Vec<String> に &"bob"(&&str 型)を渡すと、期待される &String と合わずコンパイルエラー(E0308)になるため、iter().any で比べるか、String を作って渡す。

Java 実行確認済み

import java.util.Arrays;
import java.util.List;
 
public class Main {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");
        System.out.println(fruits.contains("banana")); // true
        System.out.println(fruits.indexOf("grape"));   // -1
 
        String[] arr = {"apple", "banana", "cherry"};
        System.out.println(Arrays.asList(arr).contains("banana")); // true
 
        // int[] を asList に渡すと、要素は int ではなく int[] が 1 個になってしまう
        int[] nums = {1, 2, 3};
        System.out.println(Arrays.asList(nums).contains(2));            // false
        System.out.println(Arrays.stream(nums).anyMatch(n -> n == 2)); // true
 
        // List.of が作るリストは null を許さず、contains(null) は例外になる
        try {
            fruits.contains(null);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException");
        }
    }
}

List の contains は equals で比較する。
配列は Arrays.asList で包めば同じ書き方で探せるが、int[] のようなプリミティブ配列は要素が int ではなく int[] 1 個として扱われ、常に false になる。
List.of が返す不変リストは null を許さないので、contains(null) は NullPointerException を投げる。

C# 実行確認済み

using System;
using System.Collections.Generic;
using System.Linq;
 
class Program {
    static void Main() {
        string[] fruits = { "apple", "banana", "cherry" };
        Console.WriteLine(fruits.Contains("banana"));      // True(LINQ の拡張メソッド)
        Console.WriteLine(Array.IndexOf(fruits, "grape")); // -1
 
        // 大文字小文字を無視するなら比較子を渡す
        Console.WriteLine(fruits.Contains("BANANA", StringComparer.OrdinalIgnoreCase)); // True
 
        var list = new List<string>(fruits);
        Console.WriteLine(list.Contains("banana"));              // True
        Console.WriteLine(list.Exists(f => f.StartsWith("ch"))); // True
    }
}

配列には LINQ の Contains が使え、List<T> には自前の Contains がある。
文字列は既定で大文字小文字を区別するので、無視したいときは StringComparer.OrdinalIgnoreCase を渡す。
条件で探すなら List の Exists か LINQ の Any を使う。

つまずき

「含まれるか」を調べる書き方は言語ごとに違い、同じ in や contains という名前でも調べる対象が違うことがある。
Python の in は要素を調べるが、JavaScript の in 演算子は配列のインデックス(キー)を調べるため、"banana" in fruits は false になり、1 in fruits は true になる。
要素の有無を調べたいなら、JavaScript では includes を使う。

「同じ」と判定される基準

見つかるかどうかは、各言語がどの比較で「同じ」とみなすかで決まる。
JavaScript の includes と indexOf は参照で比べるため、中身が同じオブジェクトでも見つからず、some に条件を書く必要がある。
Python は == で比べるので、中身が同じリストなら見つかる。
NaN は自分自身とも等しくない特殊な値だが、Python の in は先に同一性を調べるため、同じ NaN オブジェクトなら True になり、別に作った NaN なら False になる。
JavaScript の includes は NaN を見つけられるが、indexOf は見つけられない。

何度も調べるときは集合にする

リストに対する in や includes や contains は、先頭から順に比べるので、要素数が増えるほど時間がかかる。
同じ配列に何度も問い合わせるなら、先に Python の set、JavaScript の Set、Java や C# や Rust の HashSet、Go の map に詰めておくと、1 回あたりの検索が平均して定数時間で済む。
要素が少ないうちは差が出ないので、問い合わせの回数が多いときに切り替えれば足りる。

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