LoginSignup
14
13

More than 5 years have passed since last update.

Enumerable.Repeat()でハマった話

Last updated at Posted at 2016-10-09

Enumerable.Repeat()の使い方ではまりんぐしたので、備忘録として投稿します。

おさらい

まずはこのメソッドついて。

Enumerable.cs
public static IEnumerable<TResult> Repeat<TResult>(TResult element, int count)

:hotsprings:効能:hotsprings:
elementで指定した要素をcount分繰り返したシーケンスを返してくれる。
戻り値そままだとIEnumerableだからクソの役にも立たない扱い辛い。
なのでチェーンメソッドでToList()やToArray()を呼び出し、指定した要素で初期化された
配列やらリストをゲットするのが定石かと。つまり、

百聞は一見にしかず.cs
 // 中身が{ 0, 0, 0 } のint[3]
var numArray = Enumerable.Repeat( 0, 3 ).ToArray();

// 中身が{ 0, 0, 0 } のList<int>
var numList = Enumerable.Repeat( 0, 3 ).ToList();

ということですな。
for文をゴリゴリ書かずとも初期化された配列を得られる。いいね!:thumbsup:

Let's 実践

さて、このめっぽう便利でうま味なEnumerable.Repeat()君に早速ひと仕事してもらうと
しましょう。初期化のターゲットとして以下のクラスを作りました。
人間を表現するクラス(ここフリ:frowning:)です。なお、職業選択の自由は保障されるべき
ものとしてJobプロパティは設定可能としました。

Person.cs
/// <summary>
/// 人間
/// </summary>
public class Person
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public Person( string name, int age, string job )
    {
        Name = name;
        Age = age;
        Job = job;
    }

    /// <summary>
    /// 名前
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// 年齢
    /// </summary>
    public int Age { get; }

    /// <summary>
    /// 職業
    /// </summary>
    public string Job { get; set; }

    /// <summary>
    /// 自己紹介
    /// </summary>
    public override string ToString()
        => $"私の名前は{Name}{Age}才、 職業は{Job}です。";
}

で、↑のクラスを使う側のコードが以下です。

Program.cs
class Program
{
    private static void Main()
    {
        // 少子化対策
        var persons = Enumerable.Repeat( 
            new Person( "クローン人間", 20, "無職" ), 5 ).ToList();

        // 仕事を与える
        var jobList = new[] { "整備士", "会計士", "弁護士", "運転士", "建築士" };

        persons = persons.Select( ( p, index ) => 
                    { p.Job = jobList[index]; return p; } ).ToList();

        // 自己紹介をお願いします
        persons.ForEach( p => Console.WriteLine( p ) );
    }
}

いささか非人道的なコードですが、細かいことを気にしてはいけないのです。
Ctrl+F5を押下して、各分野で活躍しているヒーローたちに自己紹介をしてもらいましょう。

は?

output.png

残念ながらコンパイラが腐っているようです調査します。
と、意気込んだのはいいですが、persons[0]~[4]の指し示す先のアドレスが確認できませんでした。

小一時間格闘しましたが、もういいや(投げやり)

おそらく下の図の様な事になっちまっているんでしょう。
persons[0]~[4]の指し示しているオブジェクトは同一なので、
あたかも最後に加えた変更が全てに反映されているように見えます。
(注:そう見えるだけ)

003.png

2016/10/12修正
コメント欄にて、Enumerable.Repeat()の実装を教えて頂きました。下記に引用させていただきます。
なお、.NET クラスライブラリの実装は下記サイトでガッツリ公開されておりました…
https://referencesource.microsoft.com/
結果を見るにつけ、Enumerable.Repeat()は以下のような実装になっていると思われ。

Enumerable.cs
public static IEnumerable<TResult> Repeat<TResult>(TResult element, int count) {
    if (count < 0) throw Error.ArgumentOutOfRange("count");
    return RepeatIterator<TResult>(element, count);
}
static IEnumerable<TResult> RepeatIterator<TResult>(TResult element, int count) {
    for (int i = 0; i < count; i++) yield return element;
}

(↑以前掲載していた妄想コードはプーなので削除)

そりゃTResultが参照型なら同一オブジェクトを指す「参照のコピー」がポコポコできますわな・・・。
やりたいことは下の通り異なるオブジェクトへの参照を取得する事なのですが。

004.png

解決策

てぃやあー!!(唐突)

お直し.cs
// public class Person <- 修正前
public struct Person
{
    // 以下省略

そもそも、書いてる人のおつむがアレターゲットが参照型であることに
起因する問題なので、値型に修正してリトライしてみます(強引)。
これなら異なるオブジェクトのコピーが生成されるはずです。

005.png

無事、生成されているようです:clap:

代替案

とはいえ、既に実装済みのクラスが他のクラスを継承しているとか、
引数無しのコンストラクタを必須としている場合は、上記の手抜き修正は適用できません。

で、for文を使わずに参照型の配列を正しく得られるコードがないかと
ネットの海をプカプカと浮かんでいたところ、以下のようなコードを発見。

参照型はこれでいける.cs
var persons = Enumerable.Range( 0, 5 )
                .Select( _ => new Person( "クローン人間", 20, "無職" ) ).ToArray();

ぐ、Range()とSelect()本来の使い方から逸脱しているような書き方ですが、
これなら別々の異なるオブジェクトへの参照を得られます。

2016/10/12追記
コメント欄にてより良い代替案を頂きました。(コードは、少し変更しています)

ありがとうございました.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Post._20161009
{
    public static class MyEnumerable
    {
        /// <summary>
        /// activatorを工夫して、参照型の
        /// 複数の異なる実体への参照をゲットしよう
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="activator"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public static IEnumerable<T> Repeat<T>( Func<int, T> activator, int count )
        {
            for ( int i = 0; i < count; i++ )
                yield return activator( i );
        }
    }

    class Program
    {
        private static void Main()
        {
            // 少子化対策
            // MyEnumerable.Repeat()の使用箇所
            var persons = MyEnumerable.Repeat( i => new Person( "クローン人間", 20, "無職" ), 5 ).ToList(); 

            // 仕事を与える
            var jobList = new[] { "整備士", "会計士", "弁護士", "運転士", "建築士" };

            persons = persons.Select( ( p, index ) =>
                        { p.Job = jobList[index]; return p; } ).ToList();

            // 自己紹介をお願いします
            persons.ForEach( p => Console.WriteLine( p ) );
        }
    }
}

別途、Repeat()メソッドを作成。引数activatorに参照型の複数の異な(ryを
取得するメソッドを指定する方法です。上記の例ではラムダ式
i => new Person( "クローン人間", 20, "無職" )
がこれに該当します。もちろん、値型もいけます。

結論

・Enumerable.Repeat()は値型専用。参照型に使ってはいけない(戒め)
・LINQで参照型の配列を得たいときは、Range()とSelect()の合わせ技でゲットすべし
別途それ用のメソッドを定義して対処する。

ぐちタイム

こんなんだったらEnumerable.Repeat()にstruct制約つけりゃ良かったのに・・・。
ブツクサブツクサ

14
13
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13