質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
ReactiveX

ReactiveX(Rx、Reactive Extensions)は、リアクティブプログラミングが可能なライブラリ。Java/Android用のRxJava、JavaScript用のRxJSなどさまざまな言語向けに実装されています。

.NET

.NETとは、主に.NET Frameworkと呼ばれるアプリケーションまたは開発環境を指します。CLR(共通言語ランタイム)を搭載し、入力された言語をCIL(共通中間言語)に変換・実行することが可能です。そのため、C#やPythonなど複数の言語を用いることができます。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Q&A

解決済

2回答

4074閲覧

[C#]Reactive Extensionsでasyncメソッドを順番に実行したい

soi013

総合スコア149

ReactiveX

ReactiveX(Rx、Reactive Extensions)は、リアクティブプログラミングが可能なライブラリ。Java/Android用のRxJava、JavaScript用のRxJSなどさまざまな言語向けに実装されています。

.NET

.NETとは、主に.NET Frameworkと呼ばれるアプリケーションまたは開発環境を指します。CLR(共通言語ランタイム)を搭載し、入力された言語をCIL(共通中間言語)に変換・実行することが可能です。そのため、C#やPythonなど複数の言語を用いることができます。

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

0グッド

0クリップ

投稿2021/05/25 00:51

編集2021/05/25 04:19

前提・実現したいこと

Reactive Extensionsで入力→変換→出力の"変換"の部分で非同期メソッドを使用するが、それを再入せず、順番に実行したいです。
また、変換→出力部分を後で停止できるようにもしたいです。

ここではサンプルとして、1秒毎に値を発行するObservable.Intervalを入力、文字列への変換をするメソッドConvertAsyncを変換、時刻などを付加してコンソールへプリントするメソッドWriteLineDebugを出力としています。

変換メソッドConvertAsyncは内部でawait Task.Delayをしていますが、実際に使用する場合はここがファイルやネットワークアクセスになります。
このConvertAsyncの開始・終了が前後しないように順番に行いたいです。

発生している問題・エラーメッセージ

SelectManyの場合

変換メソッドをSelectManyで呼んだ場合、変換メソッドConvertAsyncの順番が前後してしまいます。
ただし、変換→出力部分の停止はできています。

以下では0~3個目までは問題ないですが、3個目の開始Start ConvertAsync 3の後に、3個目の完了End ConvertAsync 3前に4個目が開始Start ConvertAsync 4してしまいます。

出力結果

00.065|Thread:1|Start Subscription 00.615|Thread:5|Start ConvertAsync 0 00.745|Thread:4|End ConvertAsync 0 00.750|Thread:6|Subscrive inputValue:[Converted:0 delayMilliSec:100] 01.100|Thread:6|Start ConvertAsync 1 01.210|Thread:6|End ConvertAsync 1 01.210|Thread:5|Subscrive inputValue:[Converted:1 delayMilliSec:100] 01.600|Thread:4|Start ConvertAsync 2 01.710|Thread:5|End ConvertAsync 2 01.710|Thread:4|Subscrive inputValue:[Converted:2 delayMilliSec:100] 02.102|Thread:5|Start ConvertAsync 3 02.597|Thread:5|Start ConvertAsync 4 02.838|Thread:5|End ConvertAsync 3 02.839|Thread:4|Subscrive inputValue:[Converted:3 delayMilliSec:723] 03.107|Thread:4|Start ConvertAsync 5 03.605|Thread:5|Start ConvertAsync 6 04.105|Thread:4|Start ConvertAsync 7 04.115|Thread:4|End ConvertAsync 4 04.115|Thread:4|Subscrive inputValue:[Converted:4 delayMilliSec:1513] 04.145|Thread:4|End ConvertAsync 5 04.145|Thread:4|Subscrive inputValue:[Converted:5 delayMilliSec:1029] 04.575|Thread:4|End ConvertAsync 6 04.575|Thread:4|Subscrive inputValue:[Converted:6 delayMilliSec:948] 04.605|Thread:1|Dispose Subscription 04.605|Thread:5|Start ConvertAsync 8 06.090|Thread:5|End ConvertAsync 7 06.289|Thread:5|End ConvertAsync 8

Scan+Concatの場合

変換メソッドをScan+Concatで呼んだ場合、変換メソッドConvertAsyncの順番は保たれますが、
変換部分がすぐ停止しません。出力部分だけがすぐ停止されます。

以下では5個目完了ぐらいのタイミングで停止処理disposerA.Dispose()しても、12個目まで変換は続きます。
大体止めた個数の倍ぐらいまで変換が続くようです。20個目で停止→40個目ぐらいまで変換される。

出力結果

00.065|Thread:1|Start Subscription 00.613|Thread:4|Start ConvertAsync 0 00.745|Thread:4|End ConvertAsync 0 00.751|Thread:5|Subscrive inputValue:[Converted:0 delayMilliSec:100] 01.121|Thread:5|Start ConvertAsync 1 01.233|Thread:5|End ConvertAsync 1 01.233|Thread:5|Subscrive inputValue:[Converted:1 delayMilliSec:100] 01.614|Thread:6|Start ConvertAsync 2 01.725|Thread:4|End ConvertAsync 2 01.725|Thread:4|Subscrive inputValue:[Converted:2 delayMilliSec:100] 02.112|Thread:4|Start ConvertAsync 3 03.978|Thread:6|End ConvertAsync 3 03.979|Thread:6|Start ConvertAsync 4 03.979|Thread:5|Subscrive inputValue:[Converted:3 delayMilliSec:1858] 05.823|Thread:7|End ConvertAsync 4 05.823|Thread:7|Start ConvertAsync 5 05.823|Thread:6|Subscrive inputValue:[Converted:4 delayMilliSec:1836] 06.920|Thread:1|Dispose Subscription 07.223|Thread:4|End ConvertAsync 5 07.223|Thread:4|Start ConvertAsync 6 08.545|Thread:6|End ConvertAsync 6 (略) 15.244|Thread:4|Start ConvertAsync 12 16.961|Thread:6|End ConvertAsync 12

該当のソースコード

csharp

1class Program 2{ 3 static Random rand = new Random(); 4 static Stopwatch stopwatch = new Stopwatch(); 5 6 static async Task Main(string[] args) 7 { 8 stopwatch.Start(); 9 10 //[入力] 500msec間隔で{0,1,2,3,4,5,6,,,}と流れてくる 11 var timer = Observable.Interval(TimeSpan.FromMilliseconds(500)); 12 13 WriteLineDebug($"Start Subscription"); 14 15 var disposerA = timer 16 //[変換] 文字列に変えるが、3個目以降は遅くなる 17 //Case1-SelectMany 18 .SelectMany(l => ConvertAsync(l)) 19 //Case2-Scan+Concat 20 //.Scan(Task.FromResult(""), async (previousTask, l) => 21 //{ 22 // await previousTask.ConfigureAwait(false); 23 // return await ConvertAsync(l).ConfigureAwait(false); 24 //}) 25 //.Concat() 26 27 //[出力] 現在時刻などを追加してプリント 28 .Subscribe(x => 29 WriteLineDebug($"Subscrive inputValue:[{x}]")); 30 31 //Enter押下したら、購読を停止 32 Console.ReadLine(); 33 34 WriteLineDebug("Dispose Subscription"); 35 disposerA.Dispose(); 36 37 //停止を確認 38 Console.ReadLine(); 39 } 40 41 private async static Task<string> ConvertAsync(long l) 42 { 43 WriteLineDebug($"Start ConvertAsync {l}"); 44 45 //実際はファイルアクセスやネットワークアクセスがあるとする 46 int delayMilliSec = l >= 3 ? rand.Next(600, 2000) : 100; 47 await Task.Delay(delayMilliSec).ConfigureAwait(false); 48 49 WriteLineDebug($"End ConvertAsync {l}"); 50 51 return $"Converted:{l} delayMilliSec:{delayMilliSec}"; 52 } 53 54 private static void WriteLineDebug(string message) => 55 Console.WriteLine($"{stopwatch.ElapsedMilliseconds * 0.001:00.000}|Thread:{Thread.CurrentThread.ManagedThreadId}|{message}"); 56}

試したこと

以下のブログの目次を参考に、Switch()、スケージューラーの固定を検討しましたが、期待した動作にはなりませんでした。
Reactive Extensions再入門

補足情報(FW/ツールのバージョンなど)

C# 8.0
.NET 5.0
"System.Reactive" Version="5.0.0"
VisualStudio 2019

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2021/05/25 01:22

await していないからでは?
soi013

2021/05/25 01:33

`.SelectMany(l => ConvertAsync(l))` の中で、ということでしょうか? 以下の2つのケースを試してみましたが、どちらも結果は"SelectManyの場合"と同じになりました。 `.SelectMany(async l => await ConvertAsync(l))` `.SelectMany(async l => await ConvertAsync(l).ConfigureAwait(false))`
Zuishin

2021/05/25 01:43

今調べられないので引き続き緩いコメントですが、SelectMany はタスクを受け取る引数ありましたっけ? IObservable に変換しないといけないんじゃないでしょうか?
soi013

2021/05/25 01:56

はい、SelectManyは19個(!)のオーバーロードがあり、この場合は以下のシグネチャのメソッドが呼ばれます。 `IObservable<TResult> SelectMany<TSource, TResult>(this IObservable<TSource> source, Func<TSource, Task<TResult>> selector)` なお、以下の場合ではTaskからIObservableになりますが、結果は同じです。もしかしたら上のメソッドはこれのシンタックスシュガーかもしれません。 `.SelectMany(l => ConvertAsync(l).ToObservable())`
Zuishin

2021/05/25 02:06 編集

それだと戻り値が一つのタスクだけになりそうなので、SelectMany の意味がないような。 Task オブジェクトを次に送ってるだけですよね。 SelectMany ではなく Select を使うんじゃないですか? いや、よく考えると Select でもだめですね。
soi013

2021/05/25 02:17

そうなんですよね。`SelectMany`なのに、全然"Many"じゃないんですよね。 Selectの場合は返り値が`IObservable<Task<string>>`になるので、Subscribeも少しく変わります。 ``` .Select(async l => await ConvertAsync(l)) .Subscribe(async x => WriteLineDebug($"Subscrive inputValue:[{await x}]")); ``` 結果はやはりSelectManyと同じです。 なお、`.Select(l => ConvertAsync(l))`でも同じです。
soi013

2021/05/25 04:19

Scan+Concatのケースで変換が無限には続かなかったので、修正しました。
Zuishin

2021/05/25 05:45

Select(l => ConvertAsync(l).Result) で待つことはできますが、これではだめなんですよね? 考えてみましたが、Convert を待つ間、タイマーからの出力をどこかに溜めておかなければいけないので、これ以外だと新しいオペレーターを作るか Subject を使うくらいしか思いつきませんでした。
soi013

2021/05/25 06:02

やってみたところ、`Select(l => ConvertAsync(l).Result) `の結果は期待した動作になります! ただ、`.Result`はスレッド待機させますし、デッドロックの原因になりそうで、できれば`await`したいですかね。 > 新しいオペレーターを作るか Subject を使う この部分をもう少し詳しく教えてもらうことはできますでしょうか。 Timerからの入力を一度Subjectに入れて、そこから`SelectMany`などで変換につなぐイメージでしょうか。 Subjectに入れるということはHot変換だと思いますが、それが解決にどうつながるか、上手くイメージつきません。
Zuishin

2021/05/25 06:12

デッドロックの心配であれば、Convert は l しか参照していないので大丈夫だと思いますが、Convert の仕様が変わったとしても ObserveOn や SubscribeOn でスケジューラをセットすれば大丈夫だと思います。それでデッドロックするなら await を使ってもデッドロックするでしょう。 > 新しいオペレーターを作るか Subject を使う これは、time からの出力を受け取り次第待ち行列に入れ、待ち行列が空でない限り一つずつ取り出して await Convert を呼び出し、それが終了した時点で次のオブザーバーの OnNext を呼び出すという意味です。 それをオペレーターや Subject でできると思います。
soi013

2021/05/25 07:05

一応いただいた情報をもとに自分で考えて作ってみましたが、 順番は保たれるが、Consoleからの入力を待てず(`while(true)`のせい)、3個目以降は飛び飛びになる(`FirstAsync`のせい)不思議な挙動になってしまいました。 なにか、ヒントなどありましたら、教えていただけると助かります。もちろん時間は後でも大丈夫です。 ``` var subjectInput = new Subject<long>(); var disposer0 = timer .Subscribe(l => subjectInput.OnNext(l)); var subjectConverted = new Subject<string>(); var disposerA = subjectConverted //[出力] 現在時刻などを追加してプリント .Subscribe(x => WriteLineDebug($"Subscrive inputValue:[{x}]")); while (true) { var nextValue = await subjectInput.FirstAsync(); var convertedValue = await ConvertAsync(nextValue).ConfigureAwait(false); subjectConverted.OnNext(convertedValue); } ```
Zuishin

2021/05/25 09:54

待ち行列に入れなければ取りこぼしてしまいます。 汚いですが、イメージ的にはこんな感じです。 var subject = new Subject<string>(); var disposerA = subject.Subscribe(x => WriteLineDebug($"Subscrive inputValue:[{x}]")); var queue = new Queue<long>(); var query = timer .Select(l => { queue.Enqueue(l); return Unit.Default; }) .Next() .SelectMany(_ => Enumerable.Range(0, int.MaxValue).TakeWhile(_ => queue.Any()).Select(i => queue.Dequeue())); foreach (var l in query) { subject.OnNext(await ConvertAsync(l)); }
soi013

2021/05/25 14:04

具体的なコードありがとうございます。ただこれですと、`foreach (var l in query)`が無限ループになり、その後のConsole.ReadLine()の行に到達できず、したがって停止もできません。 少し変えて、以下のように`shouldStop = true;`のように止まるかどうかを別の変数に取るようにしたら、望む動作結果にはなりました。 ただ、すごくReactive感のないコードになってしまいます。。。あと、Task.Run()内でエラーが発生するとキャッチできない気がします。 ``` bool shouldStop = false; var subject = new Subject<string>(); var disposerA = subject.Subscribe(x => WriteLineDebug($"Subscrive inputValue:[{x}]")); var queue = new Queue<long>(); var query = timer .Select(l => { queue.Enqueue(l); return Unit.Default; }) .Next() .SelectMany(_ => Enumerable.Range(0, int.MaxValue) .TakeWhile(_ => queue.Any()) .Select(i => queue.Dequeue())); //Fire&Forget var _ = Task.Run(async () => { foreach (var l in query) { if (shouldStop) return; subject.OnNext(await ConvertAsync(l)); } }); ```
Zuishin

2021/05/25 14:23

ちゃんとやるなら Replay 的なオペレーターを作るのがいいと思いますが、作っても私は使わなそうなので、手抜きで問題点だけを示しています。 問題点は、ConvertAsync を待っている間に time を取りこぼすことなので、これをどうにか解決しなければいけません。 キューにキャッシュするのが一番良さそうに思います。
soi013

2021/05/25 14:39

そうですね、`ConvertAsync`の結果を待機させる以上は、入力を保持しておく必要があるので、何らかのコレクションに貯める必要はありますよね。 上手いことその辺を隠蔽して、かつキャプチャが無いように書きたいですかね。 ちょっと考えてみます。 入力に対してReactiveに反応して処理したい、でも実行順序は守って欲しい、という要望が矛盾しているような気もしてきました。
soi013

2021/05/27 15:35

いただいた情報などを参考に色々調べた結果、Interactive.Asyncが使えそうだということが分かりました。よかったら回答を見て、コメントなどいただければ助かります。
guest

回答2

0

自己解決

Interactive.AsyncでPull/Pushの相互切り替え

いろいろ試行錯誤をした結果、Interactive.Asyncを使用して、Pull型(IAsyncEnumerable)とPush型(IObservable)の相互切り替えをするのがよいようです。

以下のコードでIObservableな入力timerからIAsyncEnumerableへ切り替え、非同期処理を順番に実行、またIObservableへ切り替えています。

csharp

1timer 2 .ToAsyncEnumerable() 3 .SelectAwait(async x => await ConvertAsync(x).ConfigureAwait(false)) 4 .ToObservable() 5 .Subscribe(x =>

RxとIx-Asyncってどう違うの?という説明は以下を見ていただくとわかりやすいです。

非同期ストリームを扱うときはRxとIx-Asyncを使い分ける - cactuaroid blog

ここでは上のコードのOperatorを順番に説明します。

ToAsyncEnumerable()

.ToAsyncEnumerable<T>()は受け取った、IObservable<T>IAsyncEnumerable<T>に変換します。後段から列挙されるたびに、IObservable<T>の入力を待つか、すでに溜まっていた入力を返します。

後段の処理ConvertAsyncは順番が前後したくないので、使う側のタイミングで値を受け取りたい、つまりPull型です。@Zuishinさんがコメントで述べられていたように、これをするためには、IObservable<T>からの入力を貯める待ち行列が必要になります。

実際、Interactive.Asyncのソースコードを見てみると、入力のOnNextConcurrentQueueEnqueueします。後段から列挙MoveNextAsyncされると、すでに溜まっていたらTryDequeue、なければ待ちます

SelectAwait

SelectAwait<TSource,TResult>は指定されたデリゲートFunc<TSource,ValueTask<TResult>>を受け取ったIAsyncEnumerable<TSource>に対して、順番に実行していきます。実際的には以下のコードのような動きです。

csharp

1await foreach (var item in source) 2{ 3 yield return await selector(item); 4}

返り値はIAsyncEnumerable<Task<TResult>>ではなく、IAsyncEnumerable<TResult>なのがポイントです。

ToObservable

ToObservable<T>では.ToAsyncEnumerable<T>()とは逆にIAsyncEnumerable<T>IObservable<T>に変換します。

ソースコードを見てみると、入力をMoveNextAsync列挙して、後段のObserverにOnNext送っています。

出力結果

入力(1000msec)よりも変換が遅くなっても順番が前後することもなく、購読を停止後に変換処理も止まっています。

shell

100.057|Thread:1|Start Subscription 200.642|Thread:4|Start ConvertAsync 0 300.744|Thread:4|End ConvertAsync 0 400.746|Thread:4|Subscrive inputValue:[Converted:0 delayMilliSec:100] 501.120|Thread:4|Start ConvertAsync 1 601.228|Thread:5|End ConvertAsync 1 701.229|Thread:5|Subscrive inputValue:[Converted:1 delayMilliSec:100] 801.628|Thread:6|Start ConvertAsync 2 901.735|Thread:4|End ConvertAsync 2 1001.735|Thread:4|Subscrive inputValue:[Converted:2 delayMilliSec:100] 1102.119|Thread:6|Start ConvertAsync 3 1203.952|Thread:6|End ConvertAsync 3 1303.952|Thread:6|Subscrive inputValue:[Converted:3 delayMilliSec:1821] 1403.953|Thread:6|Start ConvertAsync 4 1505.645|Thread:6|End ConvertAsync 4 1605.645|Thread:6|Subscrive inputValue:[Converted:4 delayMilliSec:1668] 1705.645|Thread:6|Start ConvertAsync 5 1806.851|Thread:6|End ConvertAsync 5 1906.852|Thread:6|Subscrive inputValue:[Converted:5 delayMilliSec:1196] 2006.852|Thread:6|Start ConvertAsync 6 21 2207.939|Thread:1|Dispose Subscription 2308.883|Thread:6|End ConvertAsync 6 2408.884|Thread:6|Subscrive inputValue:[Converted:6 delayMilliSec:1915]

Main全体コード

質問文とたいぶ重複しますが、Main全体のコードも貼っておきます。

csharp

1static async Task Main(string[] args) 2{ 3 stopwatch.Start(); 4 5 //[入力] 500msec間隔で{0,1,2,3,4,5,6,,,}と流れてくる 6 var timer = Observable.Interval(TimeSpan.FromMilliseconds(500)); 7 8 WriteLineDebug($"Start Subscription"); 9 10 //IX.NET方式 11 //https://cactuaroid.hatenablog.com/entry/2018/09/10/011010 12 var disposerA = timer 13 //ストリームをPull型(IAsyncEnumerable)に変換 14 .ToAsyncEnumerable() 15 //[変換] 文字列に変えるが、3個目以降は遅くなる 16 .SelectAwait(async x => await ConvertAsync(x).ConfigureAwait(false)) 17 //ストリームをPush型(IObservable)に変換 18 .ToObservable() 19 //[出力] 現在時刻などを追加してプリント 20 .Subscribe(x => 21 WriteLineDebug($"Subscrive inputValue:[{x}]"), ex => WriteLineDebug($"Subscrive Error ex:{ex.Message}"), () => WriteLineDebug($"CompletedSelectMany")); 22 23 //Enter押下したら、購読を停止 24 Console.ReadLine(); 25 26 WriteLineDebug("Dispose Subscription"); 27 disposerA.Dispose(); 28 29 //停止を確認 30 Console.ReadLine(); 31}

環境

"System.Interactive.Async" Version="5.0.0"
"System.Reactive" Version="5.0.0"

投稿2021/05/27 15:09

soi013

総合スコア149

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2021/05/27 21:29

非常にスマートな解決法ですね。
soi013

2021/05/28 11:18

Zuishinさんにそう言っていただければ安心です。ありがとうございます。
guest

0

「発行側は自由なタイミングで発行するが、消費する側はPull型でタイミングを制御したい」場合にはRxよりもChannelsがいいかもしれません。

System.Threading.Channelsを使う
https://qiita.com/skitoy4321/items/c19ca3dc7624a7049fd5

もちろん非同期ロックなり待ち行列のキューを自前実装すればRxでも実現出来ると思いますが、実装ハードルが高く保守性もやや難のあるものが出来そうな気がします。

Channelsで巨人の方を借りつつ省力で実装しつつ、空いた時間でasync/awaitの特にキャンセル処理を行儀よく実装する方法を勉強していけるといいんじゃないでしょうか。

投稿2021/05/25 13:04

tor4kichi

総合スコア769

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

soi013

2021/05/25 14:05

なるほど、`Channels`、昔さらっと読んだけど忘れていました。少し勉強して今回の解決に使えるか試してみます。
soi013

2021/05/27 15:37 編集

調べてみたのですが、ChannelsはPull型のストリームを扱うもので、特に生産:消費側が1:n or n:1 or n:nの柔軟な組み合わせが強みなようです。これはこれでRxとは別に使えそうです。 ただ今回の場合は全体としてはPush型でストリームを扱って、一部だけPullしたい、というケースでした。そしてChannelsを調べる過程で、そもそも必要なのは`IAsyncEnumerable<T>`との変換だという事に気づきました。 ですので、今回はInteractive.Asyncを使って解決するようにしました。 なお、ChannelsでもInteractive.Asyncと同様に`IAsyncEnumerable<T>`を入出力できます。 `Reader.ReadAllAsync()`とすると、`IAsyncEnumerable<T>`を受け取れます。 つまり、Push型の入力(Rx)から、並列可能な変換はRxのオペレーターで行い、 Interactive.AsyncでPull型のデータにして、 Channelsでn個の消費者から同時に消費される、なんてことが可能ですね。
tor4kichi

2021/05/28 01:30

Linqの表現を維持したままIObservable->ToAsyncEnumerable() でより良く書けるんですね。Ixをちゃんと勉強してなかった上に知ったかぶりでChannelsを提示してしまってお恥ずかしい。 soi013さんが自己解決された回答の内容もよくまとまっていて勉強になります。
soi013

2021/05/28 11:22

いえいえ、Channelsを理解する過程で、 やりたいことが、Pull型の非同期ストリームである、ということに気づくことが出来たので、tor4kichiさんのおかげです。 ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問