rksoftware

Visual Studio とか C# とかが好きです

C# Tokyo コミュニティ YouTube チャンネルの新しい動画作成方法

C# Tokyo コミュニティ YouTube チャンネルの新しい動画作成方法

日本の C# は幸せです。なぜなら日本には C# Tokyo コミュニティがあるから!

■ C# Tokyo コミュニティとは?

プログラミング言語 C# のユーザーが集まる IT 技術コミュニティです。
最近はオンラインイベントを行っていて、様子を YouTube で公開しています。

csharp-tokyo.connpass.com

www.youtube.com

■ C# Tokyo コミュニティ YouTube チャンネルの動画

C# Tokyo コミュニティ YouTube チャンネルの動画は主に 3 種類あり、次のような流れになっています。

  1. イベントの様子を限定ライブ配信 (イベント参加登録者にだけお知らせする方法でのみ視聴可能)
  2. 後日 (一週間以上後が目安)、少々の編集、目次作成、目次に合わせた字幕を付けた動画を一般公開(どなたも閲覧可能)
  3. その動画を目次に沿って切り出したショート動画を一般公開(どなたも閲覧可能)

C# エンジニアは忙しい! 皆さん 1 時間の動画だとなかなか見始められません。でもしかし、無理をすれば結構確保できる再生時間 3 分が確定しているショート動画なら安心して見始められますよね。わかります。

そんな感じで C# Tokyo コミュニティではショート動画を大切にしています。

■ しかし C# Tokyo 運営も忙しい!

ショート動画を大事にしている C# Tokyo ですが、しかし実際には作業時間がかかります。
その作業を軽減するために AI や自作のソフトで頑張ってきました。

そこから時は流れ......GitHub Copilot に来ました! エージェントの時代が!!

■ 字幕データ/ショートタイトル作成 Skill

今回、次の Skill を作成してみました。これまでコードを書いてアプリを作ったり、トークン制限と闘いながら AI と格闘していたものがあっさりです。すばらしい!

github.com

目次作成 Skill

C# Tokyo の動画作成ではまず最初に、限定ライブ配信があります。限定ライブ配信があるということは、そう! 文字起こしデータが取得できるということです。
しかし、文字起こしデータは行の区切りも調整が必要ですし、誤りも多い。何より目次データではないので、話題ごとの目次 (見出し) にする必要があります。
その中々にヘビーなタスクがなんと Skill 一個です。YouTube からダウンロードした文字起こしデータを、YouTube の説明欄に書くタイムテーブル (目次) にしてくれます。すばらしい!

github.com

タイムテーブルずらし Skill

C# Tokyo の動画には開始と終了にアイキャッチを入れています。
それがどういうことかというと、先ほど作成した目次のタイムテーブルはアイキャッチのないライブ配信時の物なので、すべての時刻をアイキャッチの分、ずらしてあげる必要があります。
10 進数でない時刻計算を目次の行数分 (30〜50 程度) 行うのは地味に手間がかかります。 その中々にヘビーなタスクがなんと Skill 一個です。すべての行の時刻を一律でずらしてくれます。すばらしい!

github.com

字幕ファイル変換 Skill

C# Tokyo の動画には、説明欄の目次に合わせて動画内にも字幕でタイトルを入れています。
この字幕はファイル読み込みで一括登録が可能なのですが、プログラマである私の手元の各種ソフトでは作成やほかの形式のファイルからの変換のできるものがありませんでした。
そこで、自作のソフトを作って変換していました。
しかし最近の AI はかしこい! かわいい! お願いすれば変換してくれます。字幕データファイルのデータフォーマット仕様まで知っているんです......。すばらしい!

github.com

ショート動画タイトル生成 Skill

最後にショート動画を作成するわけですが、切り出すタイムテーブルは先の、説明欄の目次データの中にあるのでそのまま使えます。
しかし、YouTube に公開する際にタイトルとして設定する文字列を用意しなければなりません。
1 行をキーボード操作で手早くつかんで、カット、YouTube の編集ページへペーストできると助かります。
そのためには、先の目次データファイルから

  • 各行先頭の時刻を削除
  • 末尾に YouTube らしいハッシュタグを山盛り追加

が必要です。
そう! 最近の AI はかしこい! これも Skill 一個で完了です。すばらしい!

github.com

■ PR 募集

というわけで雑に作った Skills で今回動画を作ってきました。
作った動画のイベントはこちらです。

C# Tokyo コミュニティ YouTube チャンネルはこちらです。

www.youtube.com

快適に使えていますが、まだ雑に作っただけです。改善の余地があるかもしれません。
また、おそらく結構な部分が汎用的に、つまり他の方の動画制作でも活用できると思います。そうすると改善点も出てくるはず。
ぜひ皆さん試してみて PR ください!

■ 今後の C# Tokyo

C# Tokyo コミュニティでは今後もイベントを開催、YouTube 動画公開を継続していく予定です。ぜひ今後もチェックしてみてください!

csharp-tokyo.connpass.com

C# 15 の新機能「ユニオン型」

C# 15 の新機能「ユニオン型」

C# 15 の新機能のユニオン型ですが注意があります。それが次の一点。

今は動かない

デフォルトでは今は動かないそれが特徴だけど、諦めないでください。

準備せずユニオン型を書いた場合

次のエラーになります。

error CS0518: 定義済みの型 'System.Runtime.CompilerServices.IUnion' は定義、またはインポートされていません
error CS0656: コンパイラが必要とするメンバー 'System.Runtime.CompilerServices.UnionAttribute..ctor' がありません

これは公式にも書かれていて、今は自分で作る必要があります。こんな感じみたいです。
このコードをあなたのプロジェクトのどこかに含めてください。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
    public sealed class UnionAttribute : Attribute
    {
    }

    public interface IUnion
    {
        object? Value { get; }
    }
}

ユニオン型

ユニオン型のコードはこんな感じです。
Ku 型の変数に、実態の型を代入できる点が特徴です。

Ku ku1 = new Chiyoda();
Ku ku2 = new Taito();

union Ku(Chiyoda, Taito){ }
record Chiyoda { }
record Taito { }

それ、インターフェイスでは?

いい疑問です。
ほぼ同じことができます。

IKu ku1 = new Chiyoda();
IKu ku2 = new Taito();

interface IKu { }
record Chiyoda : IKu { }
record Taito : IKu { }

しかし、敢えて似て非なるものを作ったということは違う仕様があるはず。これからの時代を作るのは我々ユニオン型だ。そういう気概があるはず。違いを見ていきましょう。

その前に

その前に、ユニオン型に含まれる型で共通のメソッド、どうするのか? 気になっていますね。わかります、私もです。見てみましょう。

インターフェイスの場合

こんな感じですよね。

IKu ku1 = new Chiyoda();
IKu ku2 = new Taito();
_ = ku1.MainStation;
_ = ku2.MainStation;

interface IKu { string MainStation { get; } }
record Chiyoda : IKu { public string MainStation => "Otemachi"; }
record Taito : IKu { public string MainStation => "Ueno"; }

ユニオン型

ユニオン型では、含まれる型の方ではなく、ユニオン型に switch で分岐するコードを実装するそうです。

Ku ku1 = new UnionTypes.Chiyoda();
Ku ku2 = new UnionTypes.Taito();
_ = ku1.MainStation;
_ = ku2.MainStation;

union Ku(Chiyoda, Taito)
{
    public string MainStation => this switch
    {
        Chiyoda c => c.MainStation,
        Taito t => t.MainStation
    };
}
record Chiyoda { public string MainStation => "Otemachi"; }
record Taito { public string MainStation => "Ueno"; }

何がうれしいのか

実装型をすべて管理できる

インターフェイスは定義後に自由に実装型を増やせます。インターフェイスを定義した人の知らない実装クラスが生まれることになります。
それがインターフェイスのうれしいことでもありますが、場合によっては全てを把握コントロールしたいこともあるでしょう。
ユニオン型は全てを定義者が管理できます。

インターフェイスの場合

interface IKu { }
record Chiyoda : IKu { }
record Taito : IKu { }

// ~ はるか遠く離れた場所 ~
// この実装が許される
record Chuo : IKu { }

ユニオン型の場合

union Ku(Chiyoda, Taito) { }
record Chiyoda { }
record Taito { }

// ~ 遠く離れていても、たとえ分かれていても ~
// この実装は Ku 型に含まれない
record Chuo { }
// Ku ku = new Chuo(); ← これはできない

switch も完全に把握できる

ユニオン型は全ての実装を把握できるので完璧な過不足のない switch のパターンを作ることができますし、完璧でない場合に警告を出してもらうこともできます。

インターフェイスの場合

次のような分岐があった場合、未知の実装型を想定するしかありません。

public string MainStation => this switch
{
    Chiyoda c => c.MainStation,
    Taito t => t.MainStation
};

そのため次のような分岐も必要になります。

Chiyoda c => c.MainStation,
Taito t => t.MainStation,
_ => "" // これがないと警告
warning CS8509: この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン '_' がカバーされていません。

ユニオン型の場合

型の種類で、分岐の不足を警告してもらえます。次の例では Taito の不足を教えてくれます。

Chiyoda c => c.MainStation,
//  Taito t => t.MainStation,
null=>"null"
この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン 'Taito' がカバーされていません。

全ての型の分岐が書かれると、警告がなくなります。

// これで警告が出ない
Chiyoda c => c.MainStation,
Taito t => t.MainStation,
null=>"null"
補足

Ku はクラスなので null があり得ます。そのため次の分岐がないと警告です。

null=>"null"
warning CS8655: この switch 式では一部の null 入力が処理されません (すべてが網羅されているわけではありません)。たとえば、パターン 'null' がカバーされていません。

ユニオン型はユニオン型

ユニオン型の変数の実体は含まれるクラスの型ではなく、ユニオン型です。

Ku ku1 = new Chiyoda();
Console.WriteLine(ku1.GetType().FullName); // Ku

そのため型キャストはできません。

Ku ku1 = new Chiyoda();

Ku ku3 = (Chiyoda)ku1;  // ← これはできない
 error CS0030: 型 'Ku''Chiyoda' に変換できません

実体の型にするには Value プロパティがあるので活用します。

Ku ku1 = new Chiyoda();

Chiyoda ku3 = (Chiyoda)ku1.Value;
Console.WriteLine(ku3.GetType().FullName); // Chiyoda

パフォーマンス?

世間にはインターフェイスを嫌う人がいます。その嫌う人のパターンの一つがインターフェイスは動作が遅くなる、です。

// インターフェイス
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;

for (long i = 0; i < times; i++)
    _ = ku1.MainStation;

long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface) : {end - start}");
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;

for (long i = 0; i < times; i++)
    _ = ku1 switch
    {
        Chiyoda c => c.MainStation,
        Taito t => t.MainStation
    };

long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Interface switch) : {end - start}");
// ユニオン型
Ku ku1 = new Chiyoda();
long start = DateTime.Now.Ticks;

for (long i = 0; i < times; i++)
    _ = ku1.MainStation;

long end = DateTime.Now.Ticks;
Console.WriteLine($"Elapsed ticks (Union) : {end - start}");

実行結果

Elapsed ticks (Interface) : 88
Elapsed ticks (Interface switch) : 122
Elapsed ticks (Union) : 194

突き詰めた時にどうかは分かりませんが、ただ単純に書き換えることでインターフェイス撲滅! ということはなさそうです。

備えよう

ユニオン型の挙動を見てみました。どこまで使うか、意外と使わなそうな気がしますが、うれしい場面があります。あるものは使っていきましょう。

サンプルコード

今回検証していたコードです。
色々書き換えながら試していたので、皆さんも書き換えながら試してください。

github.com

long times = 1000;

{
    Interfaces.IKu ku1 = new Interfaces.Chiyoda();
    Interfaces.IKu ku2 = new Interfaces.Taito();
    Console.WriteLine($"{ku1.MainStation}, {ku2.MainStation}");
    Method(ku1);
    Method(ku2);
    static void Method(Interfaces.IKu ku)
    => Console.WriteLine(ku switch
    {
        Interfaces.Chiyoda c => $"Chiyoda: {c.Akihabara}",
        Interfaces.Taito t => $"Taito: {t.Asakusa}"
    });

    Console.WriteLine(ku1.GetType().FullName);
    {
        long start = DateTime.Now.Ticks;
    }
    {
        long start = DateTime.Now.Ticks;

        for (long i = 0; i < times; i++)
            _ = ku1.MainStation;

        long end = DateTime.Now.Ticks;
        Console.WriteLine($"Elapsed ticks (Interface) : {end - start}");
    }
    {
        long start = DateTime.Now.Ticks;

        for (long i = 0; i < times; i++)
            _ = ku1 switch
            {
                Interfaces.Chiyoda c => c.MainStation,
                Interfaces.Taito t => t.MainStation
            };
        long end = DateTime.Now.Ticks;
        Console.WriteLine($"Elapsed ticks (Interface switch) : {end - start}");
    }


}

{
    UnionTypes.Ku ku1 = new UnionTypes.Chiyoda();
    UnionTypes.Ku ku2 = new UnionTypes.Taito();
    Console.WriteLine($"{ku1.MainStation}, {ku2.MainStation}");
    Method(ku1);
    Method(ku2);
    static void Method(UnionTypes.Ku ku)
    => Console.WriteLine(ku switch
    {
        UnionTypes.Chiyoda c => $"Chiyoda: {c.Akihabara}",
        UnionTypes.Taito t => $"Taito: {t.Asakusa}"
    });

    Console.WriteLine(ku1.GetType().FullName);
    Console.WriteLine(ku1.Value.GetType().FullName);

    try
    {
        UnionTypes.Chiyoda ku3 = (UnionTypes.Chiyoda)ku1.Value;
        Console.WriteLine(ku3.GetType().FullName);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

    long start = DateTime.Now.Ticks;

    for (long i = 0; i < times; i++)
        _ = ku1.MainStation;

    long end = DateTime.Now.Ticks;
    Console.WriteLine($"Elapsed ticks (Union) : {end - start}");
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
    public sealed class UnionAttribute : Attribute
    {
    }

    public interface IUnion
    {
        object? Value { get; }
    }
}

namespace Interfaces
{
    interface IKu { string MainStation { get; } }
    record Chiyoda : IKu
    {
        public string MainStation => "Otemachi";
        public string Akihabara => "Cool!";
    }
    record Taito : IKu
    {
        public string MainStation => "Ueno";
        public string Asakusa => "Nice!";
    }
    record Chuo : IKu
    {
        public string MainStation => "Tokyo";
        public string Ginza => "Great!";
    }
}

namespace UnionTypes
{
    union Ku(Chiyoda, Taito)
    {
        public string MainStation => this switch
        {
            Chiyoda c => c.MainStation,
            Taito t => t.MainStation,
            null => "null"
        };
    }
    record Chiyoda
    {
        public string MainStation => "Otemachi";
        public string Akihabara => "Cool!";
    }
    record Taito
    {
        public string MainStation => "Ueno";
        public string Asakusa => "Nice!";
    }
}

GitHub Copilot CLI をダウンロードする

GitHub Copilot CLI は winget でインストールします。

rksoftware.hatenablog.com

その実態はどこにあるのでしょう?
winget でのインストールの中身は github を見るとすぐにみつかります。

rksoftware.hatenablog.com

■ インストーラー

ここに書かれています。※間違えて少し古いバージョンを見てしまいました。 github.com

書かれている URL は ↓ 。
https://github.com/github/copilot-cli/releases/download/v1.0.9/copilot-win32-x64.zip

■ ダウンロードしてみる

こんな感じ。

Zipファイルなので展開します。

■ 実行してみる

実行してみると、インストールでなく、GitHub Copilot CLI が直接立ち上がってきます。
しかし、なんとダウンロードしたバージョンではなく現在の最新バージョンが立ち上がってきました。

■ winget でアンインストールしてみる

この環境ではすでに、GitHub Copilot CLI がインストール済みだったので、一度削除してみます。

winget uninstall GitHub.Copilot

その後また実行してみます 。

同じく最新バージョンで立ち上がってきました。

■ winget が使えない場合

もじ万が一 winget が使えない環境で GitHub Copilot CLI を使いたい場合にもしかしたら万が一使えるかもしれません。

GitHub Copilot CLI は何がうれしいのか

== 一連の記事の目次 ==

GitHub Copilot CLI をインストールして動かしてみる一連の記事の目次です。
rksoftware.hatenablog.com

== 記事本編 ==

■ ターミナルのコマンドが実行できる

どうも、ターミナルのコマンド実行できることで、コマンドの実行結果の標準出力、エラー出力に対して、AI で対処をしていきたいときに便利なんだそうです(GitHub Copilot 語る)。

■ ターミナルのコマンドを AI に作らせる

ターミナル作業を AI に頼りながら行う際に、CLI だけで手早く

  1. AI にコマンドを作らせる
  2. コマンドをすぐに実行する

が便利なんだそうです。

■ Visual Studio Code/Visual Studio 以外を IDE として使う

普段、Visual Studio Code や Visual Studio を使っているとなかなか気が付きませんが、ほかの IDE は機能の実装が遅いです。
もう、ほかの IDE を使われている方は、一思いに IDE のチャットは捨てて、CLI を使用する世界観なのかもしれません。

GitHub Copilot SDK も CLI をラップするものですし、今後 IDE の Copilot Chat 機能はもう CLI のラップで作る時代になっていくのかもしれませんね。根拠は私の勘です。

GitHub Copilot CLI で作業ディレクトリを移動する

== 一連の記事の目次 ==

GitHub Copilot CLI をインストールして動かしてみる一連の記事の目次です。
rksoftware.hatenablog.com

== 記事本編 ==

/ced コマンドまたは /cd コマンドを使用するそうです。

/cwd <移動先ディレクトリ>

コマンドを入力中に、ディレクトリの中も表示され、tab キーでの補完も聞きます。

■ 実行結果

ディレクトリ、移動しました。

GitHub Copilot CLI を IDE と接続する。

== 一連の記事の目次 ==

GitHub Copilot CLI をインストールして動かしてみる一連の記事の目次です。
rksoftware.hatenablog.com

== 記事本編 ==

以前に GitHub Copilot CLI を IDE と接続してみましたが、何が起こるのかわかりませんでした。

■ IDE と接続する

/ide コマンドで IDE と接続します。

開いている IDE のリストが出るので選択すると、接続できます。

しかし、接続しても見た目上何も起こりませんでしたし、プロンプトでリポジトリの分析をお願いしても、IDE で開いているリポジトリではなく、CLI で参照しているリポジトリについて回答してきました。

■ CLI と IDE を接続したときの使い方

というところで、どう使うのが良いのでしょう?
CLI と IDE で同じディレクトリを開いて使うのが良いそうです(GitHub Copilot 語る)。

しかし、それじゃあ、何がうれしいのか。

■ IDE で開いているファイル

IDE のエディタで開いているファイルの中身を見ないと答えられない質問をしてみます。

こんなファイル。

Visual Studio Code (IDE) で開いているコードの中のクラスのプロパティの型を聞いてみます。
するとなんと! IDE で開いているファイルへのアクセス許可を求めてきました!

許可すると、中身をちゃんと見て回答をしてくれました!

■ 結論

CLI と IDE を接続すると、IDE で開いているファイルを見てくれます。コンテキストが共有されているとはこういうことのようですね。
中々思い通りに使いこなすのは難しそうですが、これからは CLI の時代です。使っていきましょう。