20210322のC#に関する記事は9件です。

TabItemのControlTemplateを指定した際に、文字以外の部分をクリックしても反応しない

はじめに

以前の記事でTabControlのヘッダー部分を変更するコードを紹介しました。
以前紹介したコードは以下の通り

        <Style x:Key="SampleItem3" TargetType="{x:Type TabItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TabItem}">
                        <Grid SnapsToDevicePixels="true">
                            <Border x:Name="mainBorder" 
                                    BorderThickness="1,1,1,0" 
                                    Background="{TemplateBinding Background}" 
                                    BorderBrush="{TemplateBinding BorderBrush}" 
                                    CornerRadius="0,10,0,0" 
                                    Margin="0">
                                <Border x:Name="innerBorder" 
                                        Background="#FFFFFF" 
                                        BorderThickness="1,1,1,0" 
                                        BorderBrush="#ACACAC" 
                                        CornerRadius="0,10,0,0" 
                                        Margin="-1" 
                                        Opacity="0"/>
                            </Border>
                            <ContentPresenter x:Name="contentPresenter" 
                                              VerticalAlignment="Center"
                                              HorizontalAlignment="Center"
                                              ContentSource="Header" 
                                              Margin="10,0" />
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Margin" Value="-2,-2,-2,0"/>
                                <Setter Property="Opacity" TargetName="innerBorder" Value="1"/>
                                <Setter Property="BorderThickness" TargetName="innerBorder" Value="1,1,1,0"/>
                                <Setter Property="BorderThickness" TargetName="mainBorder" Value="1,1,1,0"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

その後、Darkモードにも対応しようと思い、innerBorderのBackgroundを

Background="{TemplateBinding Background}" 

としてみました。

問題発生 文字以外のところをクリックしても反応しない

すると、以前のコードではタブをクリックすれば、きちんと反応していたのに、文字の外側部分をクリックしても反応しなくなってしまいました。
image.png
図のカーソル部分をクリックしても何も反応しません・・・

たぶんなのですが、Background="{TemplateBinding Background}" としたことで、以前はBorder内が色付けされていたのが透明色になったみたいで、反応しなくなったようでした。
言い換えれば、Backgroundを色付けすればいいということになります。

でも、Darkモードに対応することを考えると、色付けはしたくないな~と思い、他に解決方法ないかなと探しました。

解決方法

これが正しい正解なのかは不安ですが、色付けしなくてもうまく動作したので紹介します。
とてもシンプルなのですが、ContentPresenter のところにLabelを使用して、その中にContentPresenterを指定するという方法です。

<ContentPresenter x:Name="contentPresenter" 
                  VerticalAlignment="Center"
                  HorizontalAlignment="Center"
                  ContentSource="Header" 
                  Margin="10,0" />

となっていたところを以下のように変更しました

    <Label HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0">
        <ContentPresenter x:Name="contentPresenter"
                            ContentSource="Header" />
    </Label>

これで、TabItemのクリックをLabelコントロールが拾ってくれるので、うまく動作しました。
他にも方法があるのかもしれませんが、数日間検討したり、調べたりしながら、ようやく見つけたので、うれしくて記事にしました(笑)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でSeleniumのTips

よく使うC#のSeleniumについてメモ書きします。

参照

hoge.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
using OpenQA.Selenium.Interactions;

chromeドライバーのお作法

hoge.cs
        private void Form1_Load(object sender, EventArgs e)
        {
               //ChromeDriverを設定する
                ChromeDriver driver = Selenium();

                // URLに移動します。
                driver.Navigate().GoToUrl(@"https://zozo.jp/");

                //Chromeドライバー終了処理
                SeleniumEnd(driver);
        }

#region ChromeDriver設定
        private ChromeDriver Selenium()
        {
            ChromeDriverService service = ChromeDriverService.CreateDefaultService();
            var options = new ChromeOptions();

            //ブラウザ非表示
            if (CB_ChromeDisp.Checked == false)
            {
                service.HideCommandPromptWindow = true;

                options.AddArgument("--headless");
                options.AddArgument("--no-sandbox");
                options.AddArgument("--window-position=-32000,-32000");
                options.AddArgument("--user-agent=hogehoge");
            }

            ChromeDriver driver = new ChromeDriver(service, options);

            return driver;

        }

#endregion

#region ChromeDriver終了処理
        private void SeleniumEnd(ChromeDriver driver)
        {
            driver.Quit();
        }

#endregion

色々な操作の仕方

hoge.cs
IWebElement element = driver.FindElement(By.Id("btnNext"));
IWebElement element = driver.FindElement(By.XPath("hogehoge"));
IWebElement element = driver.FindElement(By.ClassName("CheckExpand__label"));
IWebElement element = driver.FindElement(By.Name("Description_plain_work"));

//複数取得してループで回す処理
List<IWebElement> elements = driver.FindElements(By.ClassName("p-goods-add-cart__color")).ToList();
foreach (IWebElement ele in elements)
    {
        Console.WriteLine(ele.Text);
    }

//チェックボックスを選択する場合はSpaceを送る
IWebElement element=driver.FindElement(By.ClassName("CheckExpand__label"));
element.SendKeys(OpenQA.Selenium.Keys.Space);

//ドロップダウン
IWebElement element = driver.FindElement(By.Name("istatus"));
var selectElement = new SelectElement(element);
selectElement.SelectByIndex(3);

//クリックは.Click()では動作しない場合もある
element=driver.FindElement(By.Id("aucHTMLtag"));
element.SendKeys(OpenQA.Selenium.Keys.Return);//もしくはKeys.Enter

hoge.cs
List<IWebElement> elements = driver.FindElements(By.CssSelector("#list01 h3")).ToList();


//URLリストを定義する
List<string> url_list = new List<string>();

// 記事のタイトルをコンソールに表示
foreach (IWebElement ele in elements)
    {
        //url
        lsc_yoku_listdata_rec.Yoku_itemurl = ele.FindElements(By.TagName("a"))[0].GetAttribute("href");

        //Console.WriteLine(lsc_yoku_listdata_rec.Yoku_itemurl);         // アドレス

        //urlを配列に代入する
        url_list.Add(lsc_yoku_listdata_rec.Yoku_itemurl);

    }

//リストを配列に載せ替える
url_arr = url_list.ToArray();

//配列のカウント
url_count = url_arr.Count();

//カウント
int i = 0;

//URLを取出して個別ページの処理を行う
foreach (string url in url_arr)
{
// ループ処理の途中にキャンセルされたかを確認する
if (MainBackgroundWorker.CancellationPending)
{
//キャンセルされてた場合の処理
e.Cancel = true;
return;
}

lsc_yoku_listdata_rec.Yoku_itemurl = url;

driver.Navigate().GoToUrl(url);

driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);


// htmlファイルを読み込む
//var html = File.ReadAllText(driver.PageSource, System.Text.Encoding.UTF8);

// HTMLParserのインスタンス生成
var parser = new HtmlParser();

// htmlをパースする
var doc = parser.ParseDocument(driver.PageSource);

// idを指定してElement取得
var detail_element = doc.GetElementById("pageTop");

item_detail(detail_element);

i++;

// マルチスレット対応してテキストボックスを操作
Invoke(new Action<string>(status_mess), i +" / "+ url_count+ "件取得済み");

}

hoge.cs
            //詳細
            List<IWebElement> details = driver.FindElements(By.ClassName("p-goods-information-action")).ToList();

            int flg = 0;

            foreach (IWebElement detail in details)
            {
                string url_price = "";
                string url_size = "";
                string url_color = "";
                string url_name = "";
                string url_zaiko = "";


                url_color = detail.FindElement(By.ClassName("p-goods-add-cart__color")).Text.Replace(" ", "");
                url_size = detail.FindElements(By.TagName("span"))[2].Text.Replace("/", "").Replace(" ", ""); 


                if (url_color == cell_color && url_size == cell_size)
                {
                    //見つかったときはフラグ:1
                    flg = 1;

                    url_zaiko = detail.FindElements(By.TagName("span"))[3].Text;
                    url_name = driver.FindElement(By.ClassName("p-goods-information__heading")).Text.Replace(" ", "");

                    try
                    {
                        url_price = driver.FindElement(By.ClassName("p-goods-information__price")).Text.Replace(@"\", "").Replace(@",", "").Replace(" ", "");
                    }
                    catch
                    {
                        url_price = driver.FindElement(By.ClassName("p-goods-information__price--discount")).Text.Replace(@"\", "").Replace(@",", "").Replace(" ", "");
                    }

                    cell_price = url_price;
                    cell_color = url_color;
                    cell_name = url_name;
                    cell_zaiko = url_zaiko;

                    //セルに値を入れる
                    worksheet.Cell(i, 6).Value = cell_name;
                    worksheet.Cell(i, 16).Value = cell_zaiko;
                    worksheet.Cell(i, 9).Value = cell_price;
                    worksheet.Cell(i, 7).Value = cell_color;

                    worksheet.Cell(i, 16).Style.Fill.BackgroundColor = XLColor.White;
                }
            }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

切り取り & スケッチを起動する

クリップボードから画像取り込む機能入れる時にWindows10のWin+Shift+Sで起動する切取りを起動したかった。

基本的にはWindows FormsアプリからWindows 設定を開くと同じ

Process.Start("ms-screenclip:")

切取り実行時にアプリを最小化したい場合、Process.Startする前にWindowStateとかを最小化すれば良いのだけれど
環境によっては最小化アニメーションが終わる前に切取り実行が起動して半透明で残っていたり、
最小化せずにそのまま画面が残る事があった。

なので自分はProcess.Startする前に250msec待ちを入れて対応したけど他にやり方あるのかな(´-`)

ちなみにProcess.Startの戻りはnullになるので終了したらウィンドウを戻すという動作が出来ない。
プロセスIDをプロセス名で探すとかすれば出来そう?

起動パラメータ色々あるっぽい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]ジェネリック型のキャストの仕様に関するメモ

目標

目標は
MyItem1,MyItem2をMyItemの派生クラスとしたとき、
MyClass<MyItem1>,MyClass<MyItem2>を同じ型(例 MyClass<MyItem>)にキャストすることです。
なかなか情報が見つからなくて苦労したのでシェアしておきます。

失敗例

abstract class MyItem
{}

class MyClass<T>
where T:MyItem //TはMyItemの派生クラスであるという制限を加える。
{}

class MyItem1:MyItem
{}

class MyItem2:MyItem
{}
//テスト
public void GenericTest(){
    MyClass<MyItem1> test1=new MyClass<MyItem1>();
    MyClass<MyItem2> test2=new MyClass<MyItem2>();
    //Debug.Log((test1 as MyClass<MyItem>));  //Compile Error!
    //Debug.Log((test2 as MyClass<MyItem>));  //Compile Error!
}

自分が最初に書いたコードですが、残念ながら上手く機能しませんでした。
ちなみにDebug.Logで出力しているのはUnityを使ってるからです。適宜Cosole.WriteLineで読み替えてください。

解決策

この問題の解決策はinterfaceを用いることです。

//インターフェイスの追加
interface IMyItem
{}

interface IMyClass<out T> //out修飾子は必須!
where T:IMyItem
{}
//クラスの定義
abstract class MyItem:IMyItem
{}

class MyClass<T>:IMyClass<T> 
where T:MyItem //T:IMyItemでもok

{}

class MyItem1:MyItem
{}

class MyItem2:MyItem
{}
//テスト
public void GenericTest(){
    MyClass<MyItem1> test1=new MyClass<MyItem1>();
    MyClass<MyItem2> test2=new MyClass<MyItem2>();
    //Debug.Log((test1 as MyClass<MyItem>));  //Compile Error!
    Debug.Log((test1 as IMyClass<MyItem>));   //MyClass'1[MyItem1]
    Debug.Log(test2 as IMyClass<IMyItem>);    //MyClass'1[MyItem2]

}

だいぶ複雑というか面倒くさくなってしまいましたがこれでうまく動きます。
covariance(共変性)という仕組みが関わっていて、
公式のドキュメントがとても参考になりました。というか、ぶっちゃけ上のコードは公式の二個目の記事を読めばいらないまである。

最後にMyClassをリストにしてキャストしてみましょう。

public void GenericTest(){
    List<IMyClass<IMyItem>> list=new List<IMyClass<IMyItem>>(){
            test1,
            test2
        };
    //Debug.Log((list as List<MyClass<MyItem>>));   //Compile Error!
    Debug.Log((list as IList<MyClass<MyItem>>));    //Null
    Debug.Log((list as IList<IMyClass<MyItem>>));   //Null
    Debug.Log(list as IList<IMyClass<IMyItem>>);    //System.Collections.Generic.List`1[IMyClass`1[IMyItem]]
    var castList=(IList<IMyClass<IMyItem>>)list;
    foreach(var ele in castList){
        Debug.Log(ele); 
    }
    //foreach result
    //MyClass`1[MyItem1]
    //MyClass`1[MyItem2]
}

前のコードではIMyClass<MyItem>、IMyClass<IMyItem>両方とも正しくキャストされていましたが、
今回はIMyClassの場合もNullになってしまいました。

結論

MyClass<MyItem>の型を上手くキャストさせたいなら、
それぞれMyItemとMyClassのインターフェイスを作成して
IMyClass<IMyItem>でやりとりしよう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Revit FilteredElementCollector取得後に不要なElementを除外する

はじめに

Revitアドイン作成時、レベルや壁(Wall)のFilteredElementCollectorをいっぺんに取得してきたは良いけど、要らないやつを毎回if文で弾くのが面倒だったため、必要なものだけ絞り込む手段が欲しいなあと思いました。
調べて出てきたそれっぽい方法はあまりうまくいかなかったので、上手くいく方法をメモに残します。
そのやり方間違ってるよ、正式なやり方が別にあるよという場合はコメントいただけるとありがたいです。

環境

OS: Windows8.1
Revit: 2019.2

実装

たとえば、「建物の階」にチェックの入っているレベルを取得したい場合は以下のようにします。
Collector取得時のdocはコマンドの引数ExternalCommandData revitから取得するものです。
Document doc = revit.Application.ActiveUIDocument.Document

FilteredElementCollector GetElementsLevel_建物の階だけ(Document doc)
{
    // まず、LevelだけのFilteredElementCollectorを取得する
    FilteredElementCollector collectorLevel = new FilteredElementCollector(doc).OfClass(typeof(Level));

    // CollectorからElementIDのリストを取得する。これを除外リストとして使う
    ICollection<ElementId> exIds = collectorLevel.ToElementIds();

    // Collectorの中身を一つずつ確認
    foreach (Element el in collectorLevel)
    {
        // 念のため
        if (el.Category == null) continue;
        // 建物の階にチェックが入っているものは除外リストから外す
        if (el.LookupParameter(@"建物の階").AsInteger() > 0)
        {
            exIds.Remove(el.Id);
        }
    }
    // もし除外リストの中身があれば
    if (exIds.Count() > 0)
    {
        // 除外実行
        collectorLevel.Excluding(exIds);
    }
    return collectorLevel;
}

ちなみに「高さ」による昇順ソートはこう。FilteredElementCollecterではなくなってしまう。
(実際レベルを使うときはソートしてから使いたいだろうと思うのでついでに併記)

// 高さでソート(昇順)
IOrderedEnumerable<Level> lstLevels = from Level lv in collectorLevel orderby lv.LookupParameter(@"高さ").AsDouble() ascending select lv;

降順にしたい場合はascendingdescendingに変更。

感想

もっと他にスマートな書き方がある気がバリバリしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】ある水平軸に対して指定オブジェクトが上側にあるか、下側にあるかの判定方法

はじめに

タイトルの通り、ある水平軸に対して指定オブジェクト(今回はカメラ)が上側にあるか、下側にあるかを判定する方法についてまとめます。
上下判定01.png

Script及び注意点

まず、スクリプトから

    public bool  IsUp(Transform Target,Transform Cam)
    {
        var diff = Target.transform.position - Cam.transform.position;
        var axis = Vector3.Cross(Target.transform.forward,diff);
        return axis.x > 0;
    } 

判定自体はこの三行で行えます。

しかし、注意点として例えばTargetの原点が以下のように足元(0地点)にある場合、Target.transform.fowrdで求まる水平軸は足元にくるため、カメラが真下に潜り込まない限り基本的には判定は常にTrueが返ってきてしまいます。
上下判定02.png
よって、Targetの中央高さを基準水平軸としたい場合は、Targetの子として空のオブジェクトを配置し、そのオブジェクトをTargetにセットすることで解決します。
上下判定03.png

なぜ、この判定を行う必要があったのか

そもそもなぜこのような判定方法を取る必要があったかというと、Unityにおいて回転の値はEditor上で入力する「-50」と「310」は全く同じ角度となります。

しかしながら、この値をスクリプトから取得すると、返ってくるのはどちらの値でも「310」が返ってきます。
こうなると何が困るかというと、例えばCam01からCam02に回転値「-50」を引き継ぐ場合、以下のような事が起こりえます。
上下判定04.png
そのため、何らかの方法で回転値を判定し、正しい値を受け渡す処理が必要だったわけです。

今回はベクトルから上下を判定しましたが、調べた中でもう一つ簡単に判定する方法も見つけたので、併せて紹介しておきます。

おまけ(回転値を+180~-180に整える)

    public float AdjustAngle180(float angle)
    {
        float subNormal = Mathf.Floor((angle + 180f) / 360f) * 360f;
        return (angle > 0f) ? angle - subNormal : angle;
    }

angleに回転値を渡します、渡す際はtransform.rotation.eulerAnglesでQuaternion型からVecotr3型にキャストしてから渡しましょう。
subNormalは「0」または「360」となります、水平軸より上側なら「0」、下側なら「360」です。

これを0以上かどうか判定し、0以上の場合は取得したangleから-360してマイナスの値に、そうでなければそのままの値を返します。
上下判定05.png

結果としては先述したベクトルから求める方法と同じ結果となるため、どちらを使っても問題ないと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#/WPF/prism] ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい その2

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

■連打防止関連
ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい
https://qiita.com/tera1707/items/a6f11bd3bf2dbf97dd40
ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい その2
https://qiita.com/tera1707/items/946116bf32d0f1203006

やりたいこと

以前、掲題の内容をやりたくて、prismのDelegateCommandクラスを使って連打防止をやってみた。
ただそのときのやり方だと、同じようなことを複数のボタンでやろうとしたときに、同じようなフラグを何個も作らないといけなくなるため、もう少しマシなやり方を探していたところ、その記事に、@unidentifiedexeさんに良いやり方のコメントを頂いた。

コードのサンプルも書いて頂いて、そのまま使えそうな感じだったのだが、一応自分でも理解しておきたいということで、練習がてらコードを纏めてみたい。

サンプルコード

WPFの画面(xaml)、ViewModelと今回作成したコマンドのクラスのコードは下記の通り。(コードビハインドは省略)
image.png

MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="800">
    <StackPanel>
        <Button Content="押すと2秒間処理をし、その間は自動で無効になるボタン" FontSize="25" Command="{Binding VmMyCommand1}" Margin="20"/>
        <Button Content="ボタン1の有効無効をVMのフラグで切り替えるボタン" FontSize="25" Command="{Binding VmMyCommand2}" Margin="20"/>
    </StackPanel>
</Window>

ViewModel.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;

namespace WpfApp1
{
    class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        // ボタン押された時のCommand
        public UnRepeatableAsyncCommand VmMyCommand1 { get; private set; }
        public UnRepeatableAsyncCommand VmMyCommand2 { get; private set; }

        public bool MyCamExecuteFlag
        {
            get { return _myCamExecuteFlag; }
            set { _myCamExecuteFlag = value; OnPropertyChanged(nameof(MyCamExecuteFlag)); }
        }
        private bool _myCamExecuteFlag = true;

        public ViewModel()
        {
            // (ボタン1) 押したら2秒かかる処理を非同期で行って、その間は自動で無効になるボタン
            VmMyCommand1 = new UnRepeatableAsyncCommand(MyAsyncFunc, MyCanExecute);
            VmMyCommand1.CanExecuteChanged += ((sender, e) => Debug.WriteLine("CanExecuteChanged1"));

            // (ボタン2) ボタン1の有効無効をViewModelから切り替えるボタン
            VmMyCommand2 = new UnRepeatableAsyncCommand(async () =>
            {
                MyCamExecuteFlag = !MyCamExecuteFlag;   // CanExecuteで見るフラグ
                VmMyCommand1.RaiseCanExecuteChanged();  // ★CanExecuteが変化したことを使えないと、フラグ切り替えても有効無効変わらない!
            });
            VmMyCommand2.CanExecuteChanged += ((sender, e) => Debug.WriteLine("CanExecuteChanged2"));
        }

        // 実験用 押したときに2秒かかる処理実施
        public async Task MyAsyncFunc()
        {
            Debug.WriteLine("押された");
            await Task.Delay(2000);
            Debug.WriteLine("処理完了");
        }

        // フラグのON/OFFでボタンの有効無効を切り替える
        public bool MyCanExecute()
        {
            return MyCamExecuteFlag;
        }
    }
}

UnRepeatableAsyncCommand.cs
using System;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfApp1
{
    public class UnRepeatableAsyncCommand : ICommand
    {
        Func<Task> execute;
        Func<bool> canExecute;
        public event EventHandler CanExecuteChanged;

        // 処理中フラグ
        private bool isExecuting = false;

        public bool IsExecuting
        {
            get { return isExecuting; }
            set 
            { 
                isExecuting = value; 
                RaiseCanExecuteChanged();
            }
        }

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }

        // 本クラスを使う側が設定するCanExecuteに加え、処理中フラグのON/OFFを有効無効条件に加える
        public bool CanExecute(object parameter) => (canExecute != null) ? (canExecute() && !isExecuting) : (!isExecuting);

        // 処理実行の前後に、無効化→有効化、の処理を追加する
        public async void Execute(object parameter)
        {
            IsExecuting = true;
            await execute();
            IsExecuting = false; 
        }

        public UnRepeatableAsyncCommand(System.Func<Task> execute)
        {
            this.execute = execute;
        }

        public UnRepeatableAsyncCommand(System.Func<Task> execute, System.Func<bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
    }
}

UnRepeatableAsyncCommandが、今回作った連打防止Commandを実装したクラス。

残っている疑問

疑問

このコードの動きとしては、

  • ボタンを押すと
  • ボタンが無効化する(グレーアウトする)
  • 2秒経つと
  • ボタンが有効化する(グレーアウト解除)

という動きなのだが、
連打防止コマンドクラスの中にあるpublic async void Execute(object parameter)の中の2か所のCanExecuteChanged?.Invoke(this, EventArgs.Empty);をコメントアウトすると、ボタンが無効にはなるのだが、見た目がグレーアウトしなくなる。

自分の理解が足りてない部分なのだが、
ICommandCanExecuteChangedイベントハンドラは、CanExecuteChangedにメソッドを入れておくと、WPFのフレームワークが、CanExecuteが変化したタイミングで勝手に入れたメソッドを呼んでくれる、というものではなかったか??
(つまり、自分でそのイベントハンドラを呼ぶようなものではないと思っていた)

サンプルコードのとおり、CanExecuteChanged?.Invoke(this, EventArgs.Empty);をしてやると見た目も変わってくれるが、なぜそのような動きになるのか?が現状わかっていない...
(が、とりあえず動くものにはなったのでメモ代わりに残す...)

疑問への対応(21/03/23追記)

albireoさんからコメント頂いた内容をもとに、コードを直してみた。

  • 画面が持つボタンを、
    • ボタン1(上側のボタン)の有効無効を、ボタン2(下側のボタン)を押すと切替できるようにした。
    • その切り替えは、ViewModelが持つプロパティのONOFFで行う
      (つまりそのフラグの変化=CanExecuteの変化にする)
  • UnRepeatableAsyncCommandクラスに、CanExecuteChanged() が変化したことを知らせるためのRaiseCanExecuteChanged()を実装追加
  • ViewModelで、CanExecuteが変わるであろう部分で、該当のUnRepeatableAsyncCommandのRaiseCanExecuteChanged()を呼ぶようにした

これで、思ったことはひと通り出来てるだろうか...

参考

CanExecuteChangedめんどくさい問題を調べてみた
https://qiita.com/204504bySE/items/0c7d5ac6913673dc10f5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SelectMany の使い方

はじめに

SelectMany は、階層化されたデータを平坦化して取り出す LINQ です。同じ LINQ の Select に比べて、一段深い情報を操作するものなので理解が難しいこともあると思います。そこで、イメージ図とコード例を使って説明する記事を書いてみました。

この説明では、LINQ の Select 、ラムダ式についてはほとんど説明しませんので、これらについては知っていることを前提としています。

SelectMany は、下の絵のように、リストや配列の階層構造があるときに、深いレベルの情報をまとめて取り出すことができます。山のようになっているデータの山頂部分を一つのリストとして取得できるイメージです。

image.png

基本的な使い方

早速 C# のコードで試してみましょう。以下のコードは、 .NET Core 3.1 の環境で実行できることを確認しています。

前準備

階層構造をもつデータを定義します。

using System;
using System.Linq;

namespace select_many
{
    class Author
    {
        public string Name { get; set; }
        public Book[] Books { get; set; }
    }

    class Book
    {
        public string Name { get; set;}
    }

    class Program
    {
        static void Main(string[] args)
        {
            var authors = CreateAuthors();
        }

        static Author[] CreateAuthors()
        {
            return new[] {
                new Author()
                {
                    Name = "芥川龍之介",
                    Books = new[] {
                        new Book()
                        {
                            Name = "羅生門",
                        },
                        new Book()
                        {
                            Name = "蜘蛛の糸",
                        },
                        new Book()
                        {
                            Name = "河童",
                        },
                    },
                },

                new Author()
                {
                    Name = "江戸川乱歩",
                    Books = new[] {
                        new Book()
                        {
                            Name = "人間椅子",
                        },
                        new Book()
                        {
                            Name = "怪人二十面相",
                        },
                    },
                },

                new Author()
                {
                    Name = "川端康成",
                    Books = new[] {
                        new Book()
                        {
                            Name = "雪国",
                        },
                        new Book()
                        {
                            Name = "伊豆の踊り子",
                        },
                    },
                },
            };
        }
    }
}

Select

通常の Select だと、上の階層にあたる Author の名前一覧が取得したりできるのでした。

static void Main(string[] args)
{
    var authors = CreateAuthors();
    var authorNames = authors.Select(author => author.Name);
    Console.WriteLine(string.Join(", ", authorNames));
}
結果
芥川龍之介, 江戸川乱歩, 川端康成

青い点線で囲まれた作者名をラムダ式で指定することで、作者名の文字列のリストが取得できています。

image.png

SelectMany

SelectMany を使うと、下の階層に当たる Book の名前一覧が取得できます。

static void Main(string[] args)
{
    var authors = CreateAuthors();

    var bookNames = authors.SelectMany(
        author => author.Books.Select(book => book.Name)
    );

    Console.WriteLine(string.Join(", ", bookNames));
}
結果
羅生門, 蜘蛛の糸, 河童, 人間椅子, 怪人二十面相, 雪国, 伊豆の踊り子

Selectとは違って、引数のラムダ式 a.Books.Select(b => b.Name) では、青い点線で囲まれた部分(本の名前のシーケンス)を指定しています。この複数のシーケンスをつなげた結果が取得できます。

image.png

応用の使い方(オーバーロード)

LINQ には種々のオーバーロードが定義されていることが多いのですが、 SelectMany も同じです。複数の LINQ が必要になる操作をひとまとめにして実行できるものがオーバーロードで定義されていることが多いです。使いこなせるとよりシンプルで理解しやすい書き方ができます。

SelectMany でインデックス番号も取得

SelectMany<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,IEnumerable<TResult>>)
シーケンスの各要素を IEnumerable に射影し、結果のシーケンスを 1 つのシーケンスに平坦化します。 各ソース要素のインデックスは、その要素の射影されたフォームで使用されます。

上の階層のインデックス番号を取得します。

static void Main(string[] args)
{
    var authors = CreateAuthors();

    var bookNames = authors.SelectMany(
        (author, i) => author.Books.Select(book => $"{i}:{book.Name}")
    );

    Console.WriteLine(string.Join(", ", bookNames));
}
結果
0:羅生門, 0:蜘蛛の糸, 0:河童, 1:人間椅子, 1:怪人二十面相, 2:雪国, 2:伊豆の踊り子

余談ですが、 Select も同じ様にインデックス番号を取得できるので、2つを組み合わせると、上位と下位のインデックスをつなげて取得したりできます。

static void Main(string[] args)
static void Main(string[] args)
{`
    var authors = CreateAuthors();

    var bookNames = authors.SelectMany(
        (author, i) => author.Books.Select((book, j) => $"{i}-{j}:{book.Name}")
    );

    Console.WriteLine(string.Join(", ", bookNames));
}
結果
0-0:羅生門, 0-1:蜘蛛の糸, 0-2:河童, 1-0:人間椅子, 1-1:怪人二十面相, 2-0:雪国, 2-1:伊豆の踊り子

SelectMany で上位と下位をまとめて処理

SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>, Func<TSource,IEnumerable<TCollection>>, Func<TSource,TCollection,TResult>)
シーケンスの各要素を IEnumerable に射影し、結果のシーケンスを 1 つのシーケンスに平坦化して、その各要素に対して結果のセレクター関数を呼び出します。

上位の要素(例だとAuthor)と、下位の結果の要素(例だとbookName)をまとめて処理するラムダ式を追加できます。1つ目のラムダ式の結果の要素一つ一つに対して、2つ目のラムダ式が呼び出されます。

static void Main(string[] args)
{
    var authors = CreateAuthors();

    var bookNames = authors.SelectMany(
        author => author.Books.Select(book => book.Name),
        (author, bookName) => $"{bookName}/{author.Name}"
    );

    Console.WriteLine(string.Join(", ", bookNames));
}```

```:結果
羅生門/芥川龍之介, 蜘蛛の糸/芥川龍之介, 河童/芥川龍之介, 人間椅子/江戸川乱歩, 怪人二十面相/江戸川乱歩, 雪国/川端康成, 伊豆の踊り子/川端康成

SelectMany でインデックス番号を取得した上で、上位と下位をまとめて処理

SelectMany<TSource,TCollection,TResult>(IEnumerable<TSource>, Func<TSource,Int32,IEnumerable<TCollection>>, Func<TSource,TCollection,TResult>)
シーケンスの各要素を IEnumerable に射影し、結果のシーケンスを 1 つのシーケンスに平坦化して、その各要素に対して結果のセレクター関数を呼び出します。 各ソース要素のインデックスは、その要素の中間の射影されたフォームで使用されます。

さきほどの2つのあわせ技です。

static void Main(string[] args)
{
    var authors = CreateAuthors();

    var bookNames = authors.SelectMany(
        (author, i) => author.Books.Select((book, j) => $"{i}-{j}:{book.Name}"),
        (author, bookName) => $"{bookName}/{author.Name}"
    );

    Console.WriteLine(string.Join(", ", bookNames));
}
結果
0-0:羅生門/芥川龍之介, 0-1:蜘蛛の糸/芥川龍之介, 0-2:河童/芥川龍之介, 1-0:人間椅子/江戸川乱歩, 1-1:怪人二十面相/江戸川乱歩, 2-0:雪国/川端康成, 2-1:伊豆の踊り子/川端康成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「IEnumerableの遅延評価」と「IObservableのCold-Hot」の共通点についてまとめた

はじめに

LINQの「IEnumerableの遅延評価」と、ReactiveExtensionsの「IObservableのHod-Cold」は、どちらもLINQやRxを使い始めてしばらくするとぶち当たる壁ですね。避けては通れない道です。

先日、この両者にとある共通点を発見したので、記事にしてまとめました。

想定読者

この記事は次のような方をターゲットにしています。

  • LINQとRxをある程度使っている
  • 遅延評価やHot-Coldについての理解が曖昧

IEnumerable

まずはIEnumerableです。
コレクションクラスが必ず実装する必要があるインターフェイスですね。

IEnumerableの遅延評価

IEnumerableの遅延評価とは、「必要になるときまで計算しない」というIEnumerable<T>インターフェイスがもつ性質のことです。

例えば、以下のようなコードがあるとします。
1~10までの整数のうち偶数のみを各2倍にして変数enumerableに格納して、それをforeachで回して出力するコードです。

IEnumerable<int> enumerable =           //変数「enumerable」に
    Enumerable.Range(1, 10)             //1~10までのうち
              .Where(n => n % 2 == 0)   //偶数に絞り込んだものを
              .Select(n => n * 2);      //2倍にしたものを代入する

foreach (var num in enumerable)
{ //結果を出力
    Console.WriteLine(num);
}

もちろん結果は「4, 8, 12, 16, 20」の順に出力されるのですが、この「4, 8, 12, 16, 20」という結果は、実はforeachで回したときに初めて計算されています

普通の感覚だと、変数enumerableにLINQメソッドチェーンを代入した時点で、enumerableには「4, 8, 12, 16, 20」という計算結果が入っていると考えがちです。
しかし、実際にはenumerableに代入した時点では、「4, 8, 12, 16, 20」という計算結果は入っていません
では何が入っているのかというと、「1〜10のうち偶数に絞り込んでそれらを2倍にしてね」という「命令」が入っています

「命令」なので、計算自体はまだ実行しません。
ではいつ実行するのかというと、必要になったときに初めて計算を実行します。
つまり、foreachで回したときになって初めて、「1〜10のうち偶数に絞り込んでそれらを2倍にする」という命令が実行され、結果が出力されることになります。

これが、「必要になるときまで計算しない」というIEnumerableの遅延評価の特性です。

遅延評価の注意点

さてこの遅延評価の注意点ですが、まだ評価されていない「命令」が入ったIEnumerableを複数回foreachすると、「命令」が複数回実行されてしまうという点です。

IEnumerable<int> enumerable =   //「命令」を代入
    Enumerable.Range(1, 10)             
              .Where(n => n % 2 == 0)   
              .Select(n => n * 2);      

foreach (var num in enumerable) //「命令」を実行
{ 
    Console.WriteLine(num);
}
foreach (var num in enumerable) //「命令」を実行
{ 
    Console.WriteLine(num);
}

enumerableに入っているのはあくまで「命令」なので、2回foreachすると、命令が2回実行されてしまいます。
これは明らかに無駄な処理ですね。本来であれば、この「命令」は1回実行すれば十分で、2回目以降は最初の実行結果を流用することができるはずです。

この特性を理解していないと、無駄な処理が走るだけではなく、思わぬバグの原因となります。
次の例を見てください。

public class Hoge
{
    public Hoge()
    {
        Fugas = Enumerable.Range(1, 10)
                          .Select(n => new Fuga());
    }

    public IEnumerable<Fuga> Fugas { get; }
}

この例では、IEnumerable<Fuga>を外部に公開しています。
もうお気づきとは思いますが、外部からFugasプロパティが読まれるたびに、Fugaインスタンスが新しく10個生成されます。

これが意図した動作ならば良いのですが、「最初に生成した10個のFugaインスタンスを使いまわしたい」という場合はこのコードではうまく動かないことになります。

即時評価

IEnumerableの遅延評価の紹介を行いましたが、それと対極となる言葉が「即時評価」です。

「遅延評価」は「必要になるときまで評価しない」という考え方でしたが、
「即時評価」の場合はその名の通り「即座に評価する」となります。

遅延評価のように、「本当に必要になるときまで演算を実行しない」のではなく、「将来必要になるかどうかは分からないけど、とりあえずすべて計算して、計算した結果をキャッシュしておく」というのが即時評価の特徴です。
事前にすべての計算を完了させ、その計算結果をキャッシュするので、前述のような「無駄な処理が走る」「意図せずインスタンスが大量生成されてしまう」などといった遅延評価の懸念点は、即時評価に変更することで解決することができます。

即時評価するには、List<T>T[]などの具象オブジェクトに変換する作業が必要になります。
とは言っても、ToList()ToArray()などの拡張メソッドが用意されているため、LINQメソッドチェーンの最後にこれらを付けるだけで、即時評価に変更することができます。

IEnumerable<int> enumerable =
    Enumerable.Range(1, 10)
              .Where(n => n % 2 == 0)
              .Select(n => n * 2)
              .ToList(); //即時評価。即命令が実行されenumerableには計算結果が入る

foreach (var num in enumerable)
{ 
    Console.WriteLine(num);
}
foreach (var num in enumerable)
{ //評価済みなので2回foreachしても無駄な処理は走らない
    Console.WriteLine(num);
}

このように、遅延評価によって引き起こされる懸念を解消するためには、即時評価を適度に利用することが必要になります。

ここまでがIEnumerableの遅延評価のお話でした。
続いて、IObservableのCold-Hotのお話です。

IObservable

みんな大好きRx。RxといえばIObservable<T>
しかし、一口にIObservable<T>といっても「Cold」と「Hot」があります。
ちょうど、IEnumerable<T>にも「遅延評価」と「即時評価」があるように

HotとCold

まずは簡単にHotとColdの説明をします。

Hot

HotなIObservableとは、購読者(Observer)を複数もつことができるIObservableのことを言います。

HotなIObservableは、Subscribeされたときに、その購読者を購読者リストに保持します。
そして、値を発行するときには、購読者リストにあるすべての購読者に対して、一斉に値を発行します。

Observable(発行者)とObserver(購読者)が1:多の関係になるようなIObservableと言えます。

Cold

対してColdなIObservableとは、購読者(Observer)を1つしか持つことができないIObservableを指します。
ColdなIObservableは、購読者リストを保持する機能がなく、ただ一つの購読者しか管理することができないのが特徴です。

Observable(発行者)とObserver(購読者)が1:1の関係になるようなIObservableと言えます。

HotとColdの見分け方

HotとColdの見分け方は、値がSubject<T>クラスから送出されているかどうかで判断することができます。

HotなIObservableは複数の購読者を管理することができると書きましたが、Subject<T>クラスはまさにこの、複数の購読者(Observer)を保持し、すべての購読者に対して同時に値を発行することができる機能を持っています。
HotなIObservable<T>とは、実質的にはSubject<T>クラスそのものを指すのです。
したがって、自分でSubject<T>クラスを用意するなどして公開したIObservableは、Hotであると判断することができます。

対して、Rxに用意されている各種オペレータやファクトリメソッドの多くは、Subject<T>クラスを利用していません。
そのため、後述する一部を除き、Rxのオペレータやファクトリメソッドから生成されたIObservableはColdであると判断することができます。

ColdなIObservable

前述したように、ColdなIObservableとは、購読者(Observer)を1つしか持てないようなIObservableです。
値の発行者(Observable)と値の購読者(Observer)が1対1の関係になるようなIObservableとも言えます。

例えば、以下のコードを見てください。

//変数「observable」に1〜10までの値を発行するIObservableを代入
IObservable<int> observable = 
    Observable.Range(1, 10);

//observableを購読
observable.Subscribe(Console.WriteLine);

Observable.Rangeファクトリメソッドから生成されるIObservableはColdです。
Coldなので、購読者は1つしか持てません
この場合、購読(Subscribe)しているのは1つのみのため、何も問題なく購読することができます。

では、次のように複数の購読者がいた場合はどうなるのでしょうか。

//変数「observable」に1〜10までの値を発行するIObservableを代入
IObservable<int> observable = 
    Observable.Range(1, 10);

//observableを複数の購読者が購読
observable.Subscribe(Console.WriteLine);
observable.Subscribe(Console.WriteLine);

ColdなIObservableなのに複数回Subscribeされています。
しかしながら、この場合でも、Coldだから1回しかSubscribeを受け付けないということはなく、きちんとSubscribeした分だけ購読することができます。

ColdなIObservableは1つしか購読者が持てないのに、複数回Subscribeができるのは矛盾しているように感じます。
なぜ、ColdなIObservableなのに複数の購読者を登録できるのかというと、ObservableソースがSubscribeされた分だけ生成されているからです。
結果的には、Observableソースと購読者は1対1となるため、ColdなIObservableの要件は満たすというわけです。

ColdなIObservableの注意点

Subscribeした分だけObservableソースが生成されるとはいえ、ColdなIObservableでも複数購読が可能であれば、何が問題なのか?と思うかもしれませんが、次のような場合に注意が必要になってきます。

IObservable<int> observable =       //変数「observable」に、
    Observable.Range(1, 10)         //「1~10までの整数を発行するObservableソースを生成して
              .Where(n => n % 2)    //そのうち偶数のものだけを次に通し、
              .Select(n => n * 2);  //2倍にしたものを発行する命令」を格納

//observableを複数の購読者が購読
observable.Subscribe(Console.WriteLine);
observable.Subscribe(Console.WriteLine);

このコードでは、「1~10までの整数を発行するObservableソースを生成して、そのソースから発行された値を偶数で絞り込んで、絞り込んだ結果に対して2倍したものを発行する命令」を変数observableに代入しています。
そして、そのobservableを複数回Subscribeしています。

変数observableには、「1~10までの整数を発行するObservableソースを生成して、そのソースから発行された値を偶数で絞り込んで、絞り込んだ結果に対して2倍したものを発行する命令」が入っているのでした。
これを複数回Subscribeすれば、「1~10までの整数を発行して、それを偶数で絞り込んで、その絞り込み結果を2倍したものを発行するIObservable」が複数個複製されてしまうことになります。

つまり、Observableソースの生成処理とLINQオペレータの処理が、Subscribeされた回数分だけ走ることになってしまいます。

これが意図した動作ならば良いのですが、そうでない場合(1個のObservableソースを共有したい場合)には、無駄な処理が走ってしまったり、場合によってはバグの原因となってしまうため、注意が必要です。

HotなIObservable

Coldの対極となるのがHotです。

ColdなIObservableが1つの購読者しか持てないのに対して、
HotなIObservable複数の購読者を同時に持つことができます

つまり、HotなIObservableならば、複数回Subscribeされても、Observableソースを複製することなく、各購読者に対して値を発行することができます。
したがって、前述したようなColdなIObservableの問題点は、HotなIObservableに変換することで解決するということになります。

幸い、RxにはColdをHotに変換するオペレータが用意されているため、Hot変換したいタイミングで簡単にHot変換することができます。
その代表例が、Publish()メソッドです。

Publish()メソッドの役割を簡単に説明すると、次のようになります。

  1. 入力となるIObservable<T>Subject<T>を内部に保持する。
  2. Connect()メソッドが呼ばれると、入力のIObservableSubscribeして、得た通知をそのままSubject<T>に伝達する。
  3. Subject<T>IObservable<T>を外部に公開する。

つまり、入力となるColdなIObservableから発せられる通知を、複数の購読者を管理する機能を持つSubject<T>に仲介させることによって、複数の購読者を受付可能なHotなObservableに変換するという仕組みとなっています。

また、Connect()メソッドを呼び出す必要がありますので、Publish()メソッドの戻り値はIConnectableObservableインターフェイスとなります。

使用例としては次のようになります。

IConnectableObservable<int> observable = 
    Observable.Range(1, 10)
              .Where(n => n % 2)
              .Select(n => n * 2)
              .Publish(); //Publish以前のIObservableを保持したIConnectableObservableにする

//Publishの内部が持つSubjectを購読
observable.Subscribe(Console.WriteLine);
observable.Subscribe(Console.WriteLine);

//Publish以前のIObservableをSubscribeして、Subjectに伝える
observable.Connect();

詳しい使い方については他記事で紹介されているため、そちらを参照ください。

IEnumerableの遅延評価とIObservableのHotColdの関係性

前置きが非常に長くなりここからが本題ですが、「IEnumerableの遅延評価・即時評価」と「IObservableのHot・Cold」って、似ているものがある思いませんか?
違いを以下の表にまとめてみました。

IEnumerable IEnumerable IObservable IObservable
遅延評価 即時評価 Cold Hot
使用する命令 foreach foreach Subscribe Subscribe
変数に入っているもの コレクション操作の命令が入っている コレクションの操作済みの値そのものが入っている IObservableからの通知結果を操作する命令が入っている Observableソースそのものが入っている
特徴 複数回foreachされると命令が毎回実行される 複数回foreachされても命令の実行は1回だけ 単一の購読者しか持てない。
複数回SubscribeされるとObservableソースから複製される
複数の購読者を持てる
変換 即時評価に変換
ToList(), ToArray()など
- Hotに変換
Publish(), Multicast()など
-

変数に「命令」が入っているか「そのもの」が入っているか

1つ目の共通点として、変数に入っているものの特性が挙げられます。

「遅延評価」と「Cold」は「命令」が入っている

表からもわかるように、「IEnumerableの遅延評価」と「IObservableのCold」は、どちらも変数に「命令」が入っているということがわかります。

ここでいう「命令」というのは、具体的にはLINQオペレータによる操作のことです。
変数に「命令」、すなわちLINQオペレータによる操作が入っているということは、複数回使用するとLINQオペレータによる操作がその分毎回実行されることを意味します。

すでに挙げた例ですが、以下のような例で考えます。

IEnumerable<int> enumerable =
    Enumerable.Range(1, 10)
              .Where(n => n % 2 == 0)
              .Select(n => n * 2);

このとき、遅延評価IEnumerable<int>型の変数「enumerable」には、1〜10までの整数のコレクションを作って、偶数で絞り込んで、2倍して、という「命令」が入ります。
「命令」が入っているので、複数回使用すれば、当然「命令」も複数回実行されてしまいます。

Cold Observableの場合も同様です。

IObservable<int> observable =
    Observable.Range(1, 10)
              .Where(n => n % 2 == 0)
              .Select(n => n * 2);

ColdなIObservable<int>型の変数「observable」には、1〜10までの整数を発行するObservableソースを作って、偶数で絞り込んで、2倍して、という「命令」が入ります。
「命令」が入っているので、複数回購読すれば、当然「命令」も複数回実行されます。

「即時評価」と「Hot」は「そのもの」が入っている

「そのもの」とは、その変数から最終的に得られる情報のことです。
IEnumerableであれば、Enumerableソースから情報を受け取ってLINQによる演算をした結果そのものであるし、
IObservableであれば、Observableソースからの通知を受けてLINQによる演算を行った結果に対する購読権のことです。

最終的に得られる情報そのものが入っているので、複数回foreachしようが、複数回Subscribeしようが、LINQによる操作は重複して実行されることはありません。
最初の1回だけ実行され、あとは、計算済みの値を使い回すことができます。

「命令」から「そのもの」への変換は可能、ただし逆は不可能

2つ目の共通点として、「命令」から「そのもの」への変換は可能だが、逆は不可能という点があります。

IEnumerableならばToArray()ToList()
IObservableならばPublish()Multicast()といった、「命令」から「そのもの」への変換メソッドが用意されています。
どちらも、LINQメソッドチェーンの末尾に付けるだけで、簡単に変換することが可能です。

ただし、逆は不可能です。
一度「そのもの」、つまり演算結果になってしまったものを、「命令」、つまり演算する方法に戻すことはできないからです。

つまり何が言いたいのか

  • コレクション版LINQもRx版LINQも、ひとつのソースを複数で共有して使うことがあったら、遅延評価とColdには気をつけましょう。
  • 遅延評価やColdのまま複数箇所で共有して使用すると、ソースまで遡ってLINQ演算命令が複製されてしまいます。
  • 回避するには即時評価・Hot変換を行いましょう。

最後に

IEnumerableの遅延評価・即時評価」と「IObservableのCold・Hot」は関係が薄いように見えても、本質的な部分を見ると意外と関係があるのではないか?と思えてきたので記事にして投稿してみました。

やはり他人に説明できるように記事にすると、理解が曖昧だった部分を強制的に勉強することになるので理解が深まって良いですね。
異論やご指摘はもちろん受け付けますので、是非コメントお寄せください。

最後までありがというございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む