ときに、 巨大になる可能性のある文字列とか画像(バイナリデータ)を返したいなどの理由により、 関数が返す値として InputStream を使いたくなることがある。
そんなときは PipedInputStream と PipedOutputStream を使えば解決できるのだが、 そのとき PipedInputStream を読む処理と PipedOutputStream を書く処理は 別々のスレッドでなければいけない、という制約がある。
Javaなら普通に Thread をつくって対処してもよいのであろうが、 Kotlinには Coroutines があるので、それを使って解決する方法を調べた。
$ kotlin -version
Kotlin version 1.8.10-release-430 (JRE 17.0.9+9-Ubuntu-122.04)
hello.main.kts
@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
import kotlinx.coroutines.*
import java.io.*
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedOutputStream(pipedInputStream)
// 別スレッドにする必要がある.
GlobalScope.launch {
PrintWriter(OutputStreamWriter(pipedOutputStream, Charsets.UTF_8), true).use {
it.println("Hello,World!")
}
}
val text = pipedInputStream.bufferedReader(Charsets.UTF_8).use {
it.readText()
}
print( text )
実行します。
$ kotlin hello.main.kts
Hello,World!
できました。
それでは、本題である「大きなテキストデータを返す可能性がある関数の戻値を InputStream とする例」をつくります。
まずただ単純に String を返すだけの toText() 関数を考えます。
val toText: ()->String = {
val nameList = listOf("foo", "bar", "hoge")
val text = nameList.joinToString("\n")
text
}
println(toText())
実行すると、
foo
bar
hoge
が標準出力されます。
この toText() 関数が巨大な文字列を返す可能性がある場合に備えて、 この関数の戻値を InputStream にします。
val toInputStream: ()->InputStream = {
...
}
toText() の代わりに toInputStream() を使うコードは次のようになります。
val text = toInputStream().bufferedReader(Charsets.UTF_8).use {
it.readText()
}
print(text)
テキスト返すのが前提ならば はじめから InputStreamReader を返すべきでは? ともいえますが、例なのでよいことにします。
肝心の toInputStream() 関数の実装は、冒頭の Piped と Coroutines を使って 次のようになりました。
val toInputStream: ()->InputStream = {
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedOutputStream(pipedInputStream)
GlobalScope.launch {
PrintWriter(OutputStreamWriter(pipedOutputStream, Charsets.UTF_8), true).use {
val nameList = listOf("foo", "bar", "hoge")
it.print( nameList.joinToString("\n") )
}
}
pipedInputStream
}
完成したコードを掲載します。
@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
import kotlinx.coroutines.*
import java.io.*
/*
val toText: ()->String = {
val nameList = listOf("foo", "bar", "hoge")
val text = nameList.joinToString("\n")
text
}
println(toText())
*/
val toInputStream: ()->InputStream = {
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedOutputStream(pipedInputStream)
GlobalScope.launch {
PrintWriter(OutputStreamWriter(pipedOutputStream, Charsets.UTF_8), true).use {
val nameList = listOf("foo", "bar", "hoge")
it.print( nameList.joinToString("\n") )
}
}
pipedInputStream
}
val text = toInputStream().bufferedReader(Charsets.UTF_8).use {
it.readText()
}
print(text)
これがあれば、 大きさがわからない(巨大になるかもしれない)テキストデータや バイナリデータを返す関数をアウトオブメモリエラーなどが起きないかを心配しないで実装できそうです。
肝心の処理を別の関数として実装したい場合は suspend を使う。
この部分、
GlobalScope.launch {
PrintWriter(OutputStreamWriter(pipedOutputStream, Charsets.UTF_8), true).use {
val nameList = listOf("foo", "bar", "hoge")
it.print( nameList.joinToString("\n") )
}
}
今は単なる例なので、GlobalScope launch ブロックに直接書いていますが、 この部分がそれなりのコード分量になることがあります。
その場合は、suspend をつけた関数を定義する。
suspend fun doIt(pipedOutputStream:PipedOutputStream): Unit {
PrintWriter(OutputStreamWriter(pipedOutputStream, Charsets.UTF_8), true).use {
val nameList = listOf("foo", "bar", "hoge")
it.print( nameList.joinToString("\n") )
}
}
この doIt を使う側はつぎのように修正。
val toInputStream: ()->InputStream = {
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedOutputStream(pipedInputStream)
GlobalScope.launch {
doIt(pipedOutputStream)
}
pipedInputStream
}
以上です。