tech.guitarrapc.cóm

Technical updates

PowerShell で System.Collection.Generics.List を扱ってみる

PowerShell で Generic なクラスを扱う場面は少ないと耳にします。

目次

使わない理由: PowerShellは動的型付け

PowerShellにおいて変数は、多くの場合 Objectとして動作します。

一方で、 値(intstring) に関しては、その変数に値を代入する際、変数の型に暗黙的にキャスト(変換)を試みます。 つまり、特に数値や文字列はVariantな動作もします。

この辺は、C# の人がわかりやすいです。

このため、あまり型変換を気にすることなくサクサクかけるのが、Generic を使わないとかいう理由に上がっているようです。

Genericを使う理由 : 厳密に型チェックしたい

当然、厳密な型指定したいときは Genericなクラス(System.Collections.Generic) を利用したくなります。

ただコンパイルがないので、静的型付けチェックに難があるわけですが。

ともかく、今回は List<T> を PowerShellで使ってみましょう。

msdn - List クラス インデックスを使用してアクセスできる、厳密に型指定されたオブジェクトのリストを表します。

PowerShell で Listを扱う

List の生成

PowerShellでは、New-Objectを利用します。

仮に List<int> で作成するならこうです。

# Create Generic List for int
$list = New-Object 'System.Collections.Generic.List[int32]'

今回は、List<string> で作成します。

# Create Generic List for string
$list = New-Object 'System.Collections.Generic.List[string]'
Syste.Correction.Generic の生成をCmdlet風に行う

Genericを生成するCmdletを書いてみました。

GitHub - guitarrapc / PowerShellUtil / New-ObjectGeneric

このCmdletを利用すると、Cmdlet風にGeneric Classを生成できます。

# sample List<string>
$list = New-ObjectGeneric -className List -typeParameters "string"

# sample Dictionary<string,List<string>>
$dic = New-ObjectGeneric -className Dictionary -typeParameters ("string",$list.GetType().FullName)

パラメーターを短縮も可能です。

# sample List<string>
$list = New-ObjectGeneric List "string"

# sample Dictionary<string,List<string>>
$dic = New-ObjectGeneric Dictionary ("string", $list.GetType().FullName)
List.Add()

要素の追加は、そのままです。

# Add items to list
$list.Add("a")
$list.Add("b")
$list.Add("c")
$list.Add("d")
$list.Add("a")
$list.Add("a")
$list.Add("a")

もちろん List<int>の場合は、追加ができません。

# Add items to list
$list = New-Object 'System.Collections.Generic.List[int32]'
$list.Add("a")
Cannot convert argument "item", with value: "a", for "Add" to type "System.Int32": "Cannot convert value "a" to type "System.Int32". Error: "Input string was not in a correct format.""
At line:1 char:1
+ $list.Add("a")
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
List.AddRange()

配列の追加も可能です。

# Add range of items
$list.AddRange( [string[]]@("e","f","g","h") ) # make sure type match for list
List.Find()

リスト全体から、条件に該当する一番小さなインデックス番号の要素を返します。

C# ならば、LINQ を使って簡単に書けます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string> { };
            
            list.Add("a");
            list.Add("b");
            list.Add("c");
            list.Add("d");
            list.Add("e");

            Console.WriteLine(list.Find(x => x == "a")); // a が出力される
        }
    }
}

しかし、PowerShell には LINQ がないのです。残念ながら、本当に残念ながら。 そのため、匿名デリゲートを作ってあげる必要があります。

当然これではだめですよ。

$list.Find("a")  # return Error

PowerShell では、ScriptBlock{}が匿名デリゲートに該当します。これを使って"System.Predicate`1[System.Int32]"を生成します。

# find matched item of System.Predicate using ScriptBlock (anonymous delegates)
$list.Find({$args -eq "a"})  # return a
$list.Find({$args -ceq "A"}) # nothing return
$list.Find({$args -eq "Z"})  # nothing return
List.FindLast()

リスト全体から、条件に該当する一番大きなインデックス番号の要素を返します。

Find()と同じ要領です。

# find from last for an item of System.Predicate using ScriptBlock (anonymous delegates)
$list.FindLast({$args[0] -eq "c"}) # return c

$list.FindLast({$args[0] -eq "c"}) # return c

List.FindAll()

リスト全体から、条件に該当する要素を全て返します。

これに関してはFind()FindAdd()Where-Object の両方が使えます。

# find all matched item of System.Predicate using ScriptBlock (anonymous delegates)
$list.FindAll({$args[0] -eq "a"}) # find all a
$list | where {$_ -eq "a"}
List.FindIndex()

リストに指定したindexから最後までで、条件に該当する一番小さなインデックス番号の要素を返します。

# find index for matched item from specific index to last index. item created is System.Predicate using ScriptBlock (anonymous delegates)
$list.FindIndex(0,{$args[0] -eq "a"}) # retrun index 0
$list.FindIndex(1,{$args[0] -eq "a"}) # retrun index 5
List.FindLastIndex()

リストの先頭から指定したindexまでで、条件に該当する一番小さなインデックス番号の要素を返します。

# find index of item start from last record
$list.FindLastIndex({$args[0] -eq "a"})
List.Insert()

指定したインデックスの位置に要素を追加します。

# insert item to specific index
$list.Insert(2,"f") # f will insert to index 2
List

要素全体の取得は、そのままですね。

# list up current items
$list
List.Remove()

リストで、条件に該当する一番小さなインデックス番号の要素を削除します。

# Remove method for first match item
$list.Remove("a") # delete first a
List.RemoveAll()

リスト全体から、条件に該当する全ての要素を削除します。

# RemoveAt method to delete specific index item
$list.RemoveAt(0) # delete b
List.RemoveAt()

指定したインデックスの要素を削除します。

# RemoveAt method to delete specific index item
$list.RemoveAt(0) # delete b
List.FindLastIndex()

指定したインデックス範囲の要素を削除します。

# RemoveRange method to delete specific range items
$list.RemoveRange(0,1) # delete c and d
List.Contains()

要素がリストに存在するかどうかを判定します。

# check item is exist or not
$list.Contains("f") #true
$list.Contains("a") #false
ToArray()

List<string を stringに変換するには、C# 同様の手法をとります。

たとえば、それぞれの要素を1つの文字列に変換し、各要素は ,(カンマ) で区切るならこうです。

# output items into single String line with , foreach items
$stringarray = $list.ToArray()
$stringSeparateComma = [string]::Join(",",$stringarray)
$stringSeparateComma # c,d,e,f,g,h

区切りなしで1つの文字列にするならこうです。

# output items into single String line with , foreach items
$stringarray = $list.ToArray()
$string = [string]::Join("",$stringarray) # No separate
$string # cdefgh
Clear

リストの内容をクリアするならこうです。

# clear list
$list.Clear()

まとめ

List いいですよ。 あとは、コンパイルを (お前は何をいっている

今回紹介したサンプルコードはGitHubにおいておきます。

GitHub - guitarrapc / PowerShellUtil / Handle-GenericList / Handle-GenericList.ps1