- 投稿日:2021-03-22T21:41:42+09:00
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}"としてみました。
問題発生 文字以外のところをクリックしても反応しない
すると、以前のコードではタブをクリックすれば、きちんと反応していたのに、文字の外側部分をクリックしても反応しなくなってしまいました。
図のカーソル部分をクリックしても何も反応しません・・・たぶんなのですが、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コントロールが拾ってくれるので、うまく動作しました。
他にも方法があるのかもしれませんが、数日間検討したり、調べたりしながら、ようやく見つけたので、うれしくて記事にしました(笑)
- 投稿日:2021-03-22T17:55:02+09:00
C#でSeleniumのTips
よく使うC#のSeleniumについてメモ書きします。
参照
hoge.csusing OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Interactions;chromeドライバーのお作法
hoge.csprivate 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.csIWebElement 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.Enterhoge.csList<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; } }
- 投稿日:2021-03-22T16:21:09+09:00
切り取り & スケッチを起動する
クリップボードから画像取り込む機能入れる時にWindows10のWin+Shift+Sで起動する切取りを起動したかった。
基本的にはWindows FormsアプリからWindows 設定を開くと同じ
Process.Start("ms-screenclip:")切取り実行時にアプリを最小化したい場合、Process.Startする前にWindowStateとかを最小化すれば良いのだけれど
環境によっては最小化アニメーションが終わる前に切取り実行が起動して半透明で残っていたり、
最小化せずにそのまま画面が残る事があった。なので自分はProcess.Startする前に250msec待ちを入れて対応したけど他にやり方あるのかな(´-`)
ちなみにProcess.Startの戻りはnullになるので終了したらウィンドウを戻すという動作が出来ない。
プロセスIDをプロセス名で探すとかすれば出来そう?起動パラメータ色々あるっぽい
- 投稿日:2021-03-22T15:53:28+09:00
[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>でやりとりしよう。
- 投稿日:2021-03-22T15:20:44+09:00
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;降順にしたい場合は
ascendingをdescendingに変更。感想
もっと他にスマートな書き方がある気がバリバリしています。
- 投稿日:2021-03-22T13:45:08+09:00
【Unity】ある水平軸に対して指定オブジェクトが上側にあるか、下側にあるかの判定方法
はじめに
タイトルの通り、ある水平軸に対して指定オブジェクト(今回はカメラ)が上側にあるか、下側にあるかを判定する方法についてまとめます。
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が返ってきてしまいます。
よって、Targetの中央高さを基準水平軸としたい場合は、Targetの子として空のオブジェクトを配置し、そのオブジェクトをTargetにセットすることで解決します。
なぜ、この判定を行う必要があったのか
そもそもなぜこのような判定方法を取る必要があったかというと、Unityにおいて回転の値はEditor上で入力する「-50」と「310」は全く同じ角度となります。
しかしながら、この値をスクリプトから取得すると、返ってくるのはどちらの値でも「310」が返ってきます。
こうなると何が困るかというと、例えばCam01からCam02に回転値「-50」を引き継ぐ場合、以下のような事が起こりえます。
そのため、何らかの方法で回転値を判定し、正しい値を受け渡す処理が必要だったわけです。今回はベクトルから上下を判定しましたが、調べた中でもう一つ簡単に判定する方法も見つけたので、併せて紹介しておきます。
おまけ(回転値を+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してマイナスの値に、そうでなければそのままの値を返します。
結果としては先述したベクトルから求める方法と同じ結果となるため、どちらを使っても問題ないと思います。
- 投稿日:2021-03-22T12:46:14+09:00
[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と今回作成したコマンドのクラスのコードは下記の通り。(コードビハインドは省略)
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.csusing 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.csusing 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);をコメントアウトすると、ボタンが無効にはなるのだが、見た目がグレーアウトしなくなる。自分の理解が足りてない部分なのだが、
ICommandのCanExecuteChangedイベントハンドラは、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
- 投稿日:2021-03-22T04:34:16+09:00
SelectMany の使い方
はじめに
SelectMany は、階層化されたデータを平坦化して取り出す LINQ です。同じ LINQ の Select に比べて、一段深い情報を操作するものなので理解が難しいこともあると思います。そこで、イメージ図とコード例を使って説明する記事を書いてみました。
この説明では、LINQ の Select 、ラムダ式についてはほとんど説明しませんので、これらについては知っていることを前提としています。
SelectMany は、下の絵のように、リストや配列の階層構造があるときに、深いレベルの情報をまとめて取り出すことができます。山のようになっているデータの山頂部分を一つのリストとして取得できるイメージです。
基本的な使い方
早速 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)); }結果芥川龍之介, 江戸川乱歩, 川端康成青い点線で囲まれた作者名をラムダ式で指定することで、作者名の文字列のリストが取得できています。
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)では、青い点線で囲まれた部分(本の名前のシーケンス)を指定しています。この複数のシーケンスをつなげた結果が取得できます。
応用の使い方(オーバーロード)
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:伊豆の踊り子/川端康成
- 投稿日:2021-03-22T01:30:13+09:00
「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な
IObservableColdの対極となるのがHotです。
Coldな
IObservableが1つの購読者しか持てないのに対して、
HotなIObservableは複数の購読者を同時に持つことができます。つまり、Hotな
IObservableならば、複数回Subscribeされても、Observableソースを複製することなく、各購読者に対して値を発行することができます。
したがって、前述したようなColdなIObservableの問題点は、HotなIObservableに変換することで解決するということになります。幸い、RxにはColdをHotに変換するオペレータが用意されているため、Hot変換したいタイミングで簡単にHot変換することができます。
その代表例が、Publish()メソッドです。
Publish()メソッドの役割を簡単に説明すると、次のようになります。
- 入力となる
IObservable<T>とSubject<T>を内部に保持する。Connect()メソッドが呼ばれると、入力のIObservableをSubscribeして、得た通知をそのままSubject<T>に伝達する。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」って、似ているものがある思いませんか?
違いを以下の表にまとめてみました。
IEnumerableIEnumerableIObservableIObservable遅延評価 即時評価 Cold Hot 使用する命令 foreachforeachSubscribeSubscribe変数に入っているもの コレクション操作の命令が入っている コレクションの操作済みの値そのものが入っている 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」は関係が薄いように見えても、本質的な部分を見ると意外と関係があるのではないか?と思えてきたので記事にして投稿してみました。やはり他人に説明できるように記事にすると、理解が曖昧だった部分を強制的に勉強することになるので理解が深まって良いですね。
異論やご指摘はもちろん受け付けますので、是非コメントお寄せください。最後までありがというございました。









