20200909のC#に関する記事は5件です。

プロセスを監視して、1秒ごとに、使用中のアプリと、操作中か否かのログを書き出す、ツール

完全にディストピア感があるが、「プロセスを監視して、1秒ごとに、使用中のアプリと、操作中か否かのログを書き出す、ツール」を作った。

(毎日毎日、何をやったかを業務日報を記録する必要がある現場で、毎回手入力をするのがアホくさいので、「わかった。もう全部ログ差し出すから勝手に分析して」という気持ちで、ツール作った。)

ツール作ってみて、自身で計測してみたら、8時間ぐらいの労働時間中、PC操作(キーボードかマウス操作)しているのは、3-4割ぐらいだった。そんなもんかな、とは思った。

※c#のprocessクラスではなくwinapiを利用しているのは、c#の純正では、うまくプロセス名を拾えないケースがあったため

Process_Kanshi.cs
//c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe Process_Kanshi.cs

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Text;

public class lCnt{
    public int cntWrk { get; set; }
    public int cntSm { get; set; }
    public string PNm { get; set; }
    public string MWT { get; set; }
    public override string ToString()
    {
         return cntWrk + "\t" + cntSm + "\t" + PNm + "\t" + MWT;
    }
    public void toWin(){
        this.cntWrk++;
        this.cntSm++;
    }
    public void toLose(){
        this.cntSm++;
    }
}

public class Process_Kanshi
{ ///*


[DllImport("user32.dll")]
public static extern int GetAsyncKeyState(long vKey);

[DllImport("USER32.dll", CallingConvention = CallingConvention.StdCall)]
static extern void SetCursorPos(int X, int Y);

[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);

[DllImport("USER32.DLL")]
private static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("psapi.dll", CharSet = CharSet.Ansi)]
private static extern uint GetModuleBaseName(IntPtr hWnd, IntPtr hModule, [MarshalAs(UnmanagedType.LPStr), Out] StringBuilder lpBaseName, uint nSize);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);

  public static void Main(string[] args)
  { ///**

    //Process[] processList;

    DateTime dt = DateTime.Now;
    string dt_s = dt.ToString("yyyy_MM_dd");

    bool DtMsCr = false;
    bool DtKyBd = false;
    bool isWin = false;

    string s_mc = "";
    string lgT = "";
    string pT = "";

    string pnm0 = "";
    string mwt0 = "";

    Dictionary<string, lCnt> prcLog;// = new Dictionary<string, lCnt>();

    for(;;){ ///***

    prcLog = new Dictionary<string, lCnt>();

    for(int i = 0 ; i<60*5; i++){ //****

    //---------
    DtMsCr = false;
    DtKyBd = false;
    s_mc = Control.MousePosition.ToString();
    pnm0 = "";
    mwt0 = "";
    pT = "";
    isWin = false;
    //---------

    DateTime mdt1 = DateTime.Now;

    //processList = Process.GetProcesses();

    IntPtr hWnd = GetForegroundWindow();

    uint processId;
    GetWindowThreadProcessId(hWnd, out processId);

    if (hWnd != IntPtr.Zero)
    {
        var hnd = OpenProcess(0x0400 | 0x0010 , false, processId);

        var buffer = new StringBuilder(255);
        GetModuleBaseName(hnd, IntPtr.Zero, buffer, (uint)buffer.Capacity);

        int textLen = GetWindowTextLength(hWnd);

        StringBuilder tsb = new StringBuilder(textLen + 1);
        GetWindowText(hWnd, tsb, tsb.Capacity);

        var processName = buffer.ToString().ToLower();

        //-----
        isWin = true;
        string lmwt = tsb.ToString();
        lmwt = lmwt.Trim().Replace(" ","");
        lmwt = lmwt.Substring(0,Math.Min(30,lmwt.Length));
        pT = processName + "_" + lmwt;
        Console.WriteLine( pT );

        pnm0 = processName;
        mwt0 = lmwt;
        //-----

    }

    if(!isWin && (int)hWnd!=0){
        Console.WriteLine( "Unknown" + " _ " + hWnd);
        pT = "Unknown";
        pnm0 = "Unknown";
    }

    if((int)hWnd==0){
        Console.WriteLine( "LOG_OFF" + " _ " + hWnd);
        pT = "LOG_OFF";
        pnm0 = "LOG_OFF";
    }

    for(int ix = 32 ; ix <= 226; ix ++){
        if(GetAsyncKeyState(ix) != 0){
            DtKyBd = true;
        }
    }
    if(s_mc != Control.MousePosition.ToString()){
        DtMsCr = true;
    }


    //////////////##############
    if (!prcLog.ContainsKey(pT))
    {
        prcLog.Add(pT, new lCnt() { cntWrk=1, cntSm=1, PNm = pnm0, MWT = mwt0 });
    }else{
        if(DtKyBd || DtMsCr){
            prcLog[pT].toWin();
            //Console.WriteLine( "w");
        }else{
            prcLog[pT].toLose();
            //Console.WriteLine( "l");
        }
    }
    //////////////##############

            DateTime mdt2 = DateTime.Now;
            TimeSpan mms = mdt2-mdt1;
            int imms = (int)mms.TotalMilliseconds;
            //Console.WriteLine( "imms "+ imms);
            imms = 1000 - imms -10 ;
            if(imms>=1000 || imms < 0 ){imms=700;}
        System.Threading.Thread.Sleep(imms);

    } //****

    dt = DateTime.Now;
    string dt_sl = dt.ToString("yyyy_MM_dd_HHmmss");

    lgT = "";
    foreach (KeyValuePair<string, lCnt> kvp in prcLog)
    {
        lgT = lgT + dt_sl + "\t" + kvp.Key + "\t" +  kvp.Value + Environment.NewLine;
    }

    System.IO.File.AppendAllText(@".\" + dt_s + ".txt", lgT);
    lgT = "";
    pT = "";

    pnm0 = "";
    mwt0 = "";

    DtMsCr = false;
    DtKyBd = false;
    s_mc = Control.MousePosition.ToString();


    } ///***

  } ///**

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

【C#】競技プログラミング 入出力処理

入力

文字列1行

string str = Console.ReadLine();

int配列

int[] array = Console.ReadLine().Split().Select(int.Parse).ToArray();

long配列

long[] array = Console.ReadLine().Split().Select(long.Parse).ToArray();

Console.ReadLine().Split()までの部分で関数を作っておくと良いかもしれません。

N Mなどの複数の時

以下のクラスをコピペして貼る。
(atcoder社長のchokudai氏の利用しているScannerを一部改変したもの)

class Scanner
{
    string[] s;
    int i;
    char[] cs = new char[] { ' ' };
    public Scanner()
    {
        s = new string[0];
        i = 0;
    }

    public string Next()
    {
        if (i < s.Length) return s[i++];
        string st = Console.ReadLine();
        while (st == "") st = Console.ReadLine();
        s = st.Split(cs, StringSplitOptions.RemoveEmptyEntries);
        if (s.Length == 0) return Next();
        i = 0;
        return s[i++];
    }

    public int NextInt()
    {
        return int.Parse(Next());
    }

    public long NextLong()
    {
        return long.Parse(Next());
    }

使い方

var scanner = new Scanner();
//N M K
int N = NextInt();
long M = NextLong();
string K = Next();

出力

基本

//改行も入ります
var num = 20;
Console.WriteLine(num); 
Console.WriteLine("Yes");
Console.WriteLine("No");

フォーマット

var n = 12, m = 24;
Console.WriteLine("{0} {1} {2}", n, m, 4 + 4);

変数名直接(古いコンパイラでは対応していないかも)

var n = 12, m = 24;
Console.WriteLine("${n} ${m}");

配列

var array = new int[]{ 20, 30, 22, 11 };
//コンマ区切り
Console.WriteLine(string.Join(",", array));
//スペース区切り
Console.WriteLine(string.Join(" ", array));
//改行区切り
Console.WriteLine(string.Join("\r\n", array));

foreachを使ったやつ。atcoderだと、一番最後に空白あっても問題ないため

var array = new int[]{ 20, 30, 22, 11 };
foreach(var a in array)
{
    Console.WriteLine("{0} ", a);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XAML構築時に利用したやつ簡易メモ

:qiitan:

XAMLでWPF用のUIを構築する際のメモ
検索して探した結果などのコピペ的まとめもありまするメモ

APPのウィンドウ枠を非表示にして透明化

APPを閉じたりするバーなどを非表示にしてAPPの枠がない状態へ設定

WindowStyle : None
AllowTransparency : true

カスタムコントロールからAPPの閉じるなどのバーを自作

自作したAPPヘッダーでAPP拡大や移動など制御したい
カスタムコントロールにするとWindowがないから動かなかったので対処をメモ

public static class WindowUtil
{
  public static Window GetWindow(this DependencyObject element)
  {
    return Window.GetWindow(element);
  }
}
private void OnClose(object sender, RoutedEventArgs e)
{
  var window = ((FrameworkElement)sender).GetWindow();
  window.Close();
}

private void OnMove(object sender, MouseButtonEventArgs e)
{
  var window = ((FrameworkElement)sender).GetWindow();
  window.DragMove();
}

private void OnMaximam(object sender, RoutedEventArgs e)
{
  var window = ((FrameworkElement)sender).GetWindow();
  window.WindowState = window.WindowState != WindowState.Maximized ? WindowState.Maximized : WindowState.Normal;
}

private void OnMimimam(object sender, RoutedEventArgs e)
{
  var window = ((FrameworkElement)sender).GetWindow();
  window.WindowState = window.WindowState != WindowState.Minimized ? WindowState.Minimized : WindowState.Normal;
}

ボタンの見た目を変更

カンバスにSVGみたいな言語で図形が描画できる
このへんは便利と感じる、SVGと同じことできる感じ

<Style TargetType="Button">
  <!--Set to true to not get any properties from the themes.-->
  <Setter Property="OverridesDefaultStyle" Value="True"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Ellipse Fill="{TemplateBinding Background}"/>
          <ContentPresenter HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

カスタムコントロールのXAMLでへ値をプロパティ譲渡

public class ClockControl: UserControl
    {
        public static readonly DependencyProperty CityProperty = DependencyProperty.Register
            (
                 "City", 
                 typeof(string), 
                 typeof(ClockControl), 
                 new PropertyMetadata(string.Empty)
            );

        public string City
        {
            get { return (string)GetValue(CityProperty); }
            set { SetValue(CityProperty, value); }
        }

        public ClockControl()
        {
            InitializeComponent();

        }
}

グリッドレイアウトでレスポンシブ

*がついているやつは伸びる感じします

<Grid>
  <Grid.RowDefinitions>
      <RowDefinition Height="1080*"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
      <ColumnDefinition Width="240*"/>
      <ColumnDefinition Width="240*"/>
      <ColumnDefinition Width="240*"/>
  </Grid.ColumnDefinitions>
  <StackPanel Grid.Column="0"></StackPanel>
  <StackPanel Grid.Column="1"></StackPanel>
  <StackPanel Grid.Column="3"></StackPanel>
</Grid>

参考サイト

XAML でのレスポンシブ レイアウト
https://docs.microsoft.com/ja-jp/windows/uwp/design/layout/layouts-with-xaml

xamlからパラメーターをどのように渡しますか?
https://www.it-swarm.dev/ja/c%23/xaml%E3%81%8B%E3%82%89%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%BC%E3%82%92%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E6%B8%A1%E3%81%97%E3%81%BE%E3%81%99%E3%81%8B%EF%BC%9F/972009094/

ControlTemplate クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.controls.controltemplate?view=netcore-3.1

How to: Draw an Ellipse or a Circle
https://docs.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/how-to-draw-an-ellipse-or-a-circle?view=netframeworkdesktop-4.8

ウィンドウ枠のない WPF アプリを作成する
https://sakapon.wordpress.com/2015/03/01/wpf-borderless/

【WPF】包含するコンテンツからWindowを取得するには?
http://pro.art55.jp/?eid=1070343

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

IvyFEM(.NET向け有限要素法ライブラリ)をPythonから使用する

1. はじめに

.NET向け有限要素法ライブラリIvyFEM.dllを開発中です。
C#で利用することを想定したライブラリですが、Pythonからも利用できそうなことがわかったので記事にします。

2. Windows10でPythonから.NETを呼び出す方法(Python.NET)

Python.NETを使うことにしました。
http://pythonnet.github.io/

以下、Windows環境にPythonが既にインストールされていることとします。

2.1. Python.NETのインストール

コマンドプロンプトから以下を実行してください。

py -m pip install pythonnet

3. IvyFEM.dllパッケージ

GithubのIvyFEMページから最新のパッケージをダウンロードして、作業ディレクトリに展開してください(IvyFEMページのインストール方法を参照してください、VC++のruntimeが必要)。
IvyFEM: https://github.com/ryujimiya/IvyFEM/

展開すると以下のようになります。
20200909_python_ivyfem_example_files.jpg

4. PythonからIvyFEMを呼び出す

PythonでPoissonの方程式を解いてみました。

正方形領域をとり、辺上はポテンシャル0の境界条件を課しています。
領域内部に円の領域をとり、その領域に電荷を印加したときの領域全体のポテンシャル分布を計算しています。
20200909_python_ivyfem_example_Cad.jpg

main2.py
import clr

clr.AddReference("System")
from System import UInt32

clr.AddReference("System.Collections")
from System.Collections.Generic import List, IList

clr.AddReference('IvyFEM')
from IvyFEM \
import \
Cad2D, Mesher2D, FEWorld, \
FiniteElementType, \
PoissonMaterial, \
CadElementType, FieldValueType, FieldFixedCad, \
Poisson2DFEM

clr.AddReference('IvyFEM')
from IvyFEM.Linear \
import \
LapackEquationSolver, LapackEquationSolverMethod, \
IvyFEMEquationSolver, IvyFEMEquationSolverMethod

clr.AddReference('OpenTK')
from OpenTK import Vector2d

cad = Cad2D()

pts = List[Vector2d]()
pts.Add(Vector2d(0.0, 0.0))
pts.Add(Vector2d(1.0, 0.0))
pts.Add(Vector2d(1.0, 1.0))
pts.Add(Vector2d(0.0, 1.0))
lId1 = cad.AddPolygon(pts).AddLId
print("lId1 = " + str(lId1))

# ソース
lId2 = cad.AddCircle(Vector2d(0.5, 0.5), 0.1, lId1).AddLId
print("lId2 = " + str(lId2))

eLen = 0.05
mesher = Mesher2D(cad, eLen)

# 座標を参照
vecs = mesher.GetVectors()
cnt = len(vecs)
print("len(vecs) = " + str(cnt))
coId = 0
for vec in vecs:
    print("vec[" + str(coId) + "] " + str(vec.X) + ", " + str(vec.Y))
    coId += 1

world = FEWorld()
world.Mesh = mesher

dof = 1 # スカラー
feOrder = 1
quantityId = world.AddQuantity(dof, feOrder, FiniteElementType.ScalarLagrange)

world.ClearMaterial()
ma1 = PoissonMaterial()
ma1.Alpha = 1.0
ma1.F = 0.0
ma2 = PoissonMaterial()
ma2.Alpha = 1.0
ma2.F = 100.0
maId1 = world.AddMaterial(ma1)
maId2 = world.AddMaterial(ma2)

lId1 = 1
world.SetCadLoopMaterial(lId1, maId1)

lId2 = 2
world.SetCadLoopMaterial(lId2, maId2)

zeroEIds = [1, 2, 3, 4]
zeroFixedCads = List[FieldFixedCad]()
for eId in zeroEIds:
    #スカラー
    # オーバーロードであることに注意
    fixedCad = FieldFixedCad.Overloads[UInt32, CadElementType, FieldValueType](eId, CadElementType.Edge, FieldValueType.Scalar)
    zeroFixedCads.Add(fixedCad)
world.SetZeroFieldFixedCads(quantityId, zeroFixedCads)

#DEBUG
zeroFixedCads = world.GetZeroFieldFixedCads(quantityId)
print("len(zeroFixedCads) = " + str(len(zeroFixedCads)))

world.MakeElements()

feIds = world.GetTriangleFEIds(quantityId)
feCnt = len(feIds)
print("feCnt = " + str(feCnt))
for feId in feIds:
    print("--------------")
    print("feId = " + str(feId))
    triFE = world.GetTriangleFE(quantityId, feId)
    nodeCoIds = triFE.NodeCoordIds
    elemNodeCnt = nodeCoIds.Length
    for iNode in range(elemNodeCnt):
        print("coId[" + str(iNode) + "] = " + str(nodeCoIds[iNode]))

# ポアソン方程式FEM
FEM = Poisson2DFEM(world)

# リニアソルバー
'''
solver = LapackEquationSolver()
solver.IsOrderingToBandMatrix = True
solver.Method = LapackEquationSolverMethod.Band
FEM.Solver = solver
'''

solver = IvyFEMEquationSolver()
solver.Method = IvyFEMEquationSolverMethod.NoPreconCG
FEM.Solver = solver

# 解く
FEM.Solve()
U = FEM.U

# 結果
nodeCnt = len(U)
for nodeId in range(nodeCnt):
    print("-------------")
    coId = world.Node2Coord(quantityId, nodeId)
    value = U[nodeId]
    print(str(nodeId) + " " + str(coId) + " " + "{0:e}".format(value))

5. 実行

py -m main2

5. まとめ

IvyFEM.dllを使ってPythonからPoissonの方程式を解きました。

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

【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう

はじめに

C#を始めとするオブジェクト指向言語には「インターフェイス」という機能があります。
これを使うと良い設計になるというのはよく言われていますが、具体的にインターフェイスを使うとどう良いことがあるのか、というのは実感しづらい人も多いと思います。

僕もC#学びたての頃はほんとうにインターフェイスの利点が理解できず苦しみました。しかし、この記事で説明する「インターフェイスには3つのタイプがある」ことを理解して以来、もうインターフェイスが便利すぎて、インターフェイスなしではコーディングできない体質になってしまいました

そこでこの記事では、インターフェイスを使う利点がいまいち理解できていない人が、インターフェイスを使いたくて使いたくて仕方がなくなるようにすることを目的として書きました。

注意点として、僕はC#の開発者でもなければ指導者でもないので、あくまで個人的な意見として参考にしていただけるとうれしいです。
間違っていたり、意見がお有りの方は、ぜひコメントでお知らせください。

インターフェイスについて「よくある間違い」

インターフェイスについて、以下のような間違いがよくされています。

  • インターフェイスは、ポリモーフィズムを実現するためだけに存在する
  • インターフェイスは、複数のクラスに実装しないと意味がない

僕もC#学びたての頃はよくこのような勘違いをしていて、そのせいでまったく理解が進みませんでした。
しかし、これらは間違いです。

インターフェイスは、これによってポリモーフィズムが実現されなくても大きな意味があるし、たった一つのクラスにしか実装されないインターフェイスにも、重要な意味があります

これらの混乱が生じる原因として、一言に「インターフェイス」と言っても3つのタイプが存在するからではないかと考えました。

3つのタイプのインターフェイス

インターフェイスは、以下の3つのタイプに分けられます。

1. 疎結合を目的としたインターフェイス
2. クラスの機能を保証することを目的としたインターフェイス
3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

コードだけを見ていると、どのインターフェイスも同じように使われているように見えますが、実はその目的は全く異なることがあるわけです。

よくあるインターフェイスの説明は分かりづらい

また、インターフェイスの説明は分かりづらいものが多すぎます。
「C# インターフェイス とは」などで調べると出てくるよくある説明に次のようなものがあります。

インターフェイスは、実装するクラスにメソッドの実装を強制するものです

これがよくわからないんですよね。
メソッドの実装を強制されるとどんないいことがあるのかわかりません。

また、「インターフェイス」という名前からも想像がつかない機能というか。
なぜ「メソッドの実装を強制する」という機能に「インターフェイス」という名前が付けられているのかわかりません

これは、上記の「インターフェイスは、実装するクラスにメソッドの実装を強制するものです」という説明が、インターフェイスの本質を説明したものではないから、混乱が生じるんだと思います。

じゃあインターフェイスの本質ってなに

インターフェイスとは「インターフェイス」です。
「ユーザーインターフェイス」とかの「インターフェイス」と同じ意味です。

では何のインターフェイスかというと、クラスのインターフェイスです。
もっと言うと、「クラスにアクセスするためのインターフェイス」といえます。

「クラスにアクセスするためのインターフェイス?別にインターフェイスがなくても普通にインスタンス名.メンバ名でアクセスできるが?」と思うかもしれません。
ところが、インターフェイスがないと大きな問題となる場合がたくさんあります。

QiitaPostクラスで考えてみる

例えば以下のような、Qiitaの記事を表すQiitaPostクラスを考えてみます。

class QiitaPost 
{
    private string m_title;
    private string m_text;

    /// <summary>
    /// タイトルと本文を入力して、記事を新規作成します。
    /// </summary>
    /// <param name="title">記事タイトル</param>
    /// <param name="text">記事本文</param>
    public QiitaPost(string title, string text)
    {
        this.m_title = title;
        this.m_text = text;
    }
    /// <summary>
    /// 記事のURL
    /// </summary>
    public Uri PostURL { get; }
    /// <summary>
    /// 記事タイトル
    /// </summary>
    public string Title => m_title;
    /// <summary>
    /// 記事本文
    /// </summary>
    public string Text => m_text;
    /// <summary>
    /// LGTM数
    /// </summary>
    public int LGTMCount { get; private set; }
    /// <summary>
    /// ストック数
    /// </summary>
    public int StockCount { get; private set; }

    /// <summary>
    /// LGTMする
    /// </summary>
    public void LGTM()
    {
        ++LGTMCount;
    }
    /// <summary>
    /// ストックする
    /// </summary>
    public void Stock()
    {
        ++StockCount;
    }
    /// <summary>
    /// 記事を削除する
    /// </summary>
    public void Delete()
    {
        m_title = string.Empty;
        m_text = string.Empty;
    }
}

Qiitaの記事を書く人

Qiitaはまず記事がなきゃ始まりません。
記事を書く人がこのQiitaPostクラスのインスタンスを生成しました。

//Qiitaの記事を書いた!
QiitaPost post = new QiitaPost("タイトル", "本文");

これをQiitaのサーバーにアップロードします。

//Qiitaのサーバーに記事をアップロードした!
QiitaServer.Upload(post);

これで晴れてみんなに読んでもらえます。

Qiitaの記事を読む人

Qiitaの記事を読む人は、QiitaServerから記事を取得してタイトルと本文を確認します。

//Qiita記事を取得
QiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

記事を読んだあとに良かったと思ったのでLGTMもしちゃいました。

問題点

さて、このQiitaPostクラスの問題点は、次のようなことができてしまう点です。

//Qiita記事を取得
QiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

//勝手に人の書いた記事を消す!!
downloadedPost.Delete();

なんと、読み手が勝手に人の書いた記事を消せてしまいます。
そりゃそうですよね、何しろQiitaPostクラスにはDeleteメソッドがpublicで定義されているのですから…。

また、次のような問題も発生します。

//記事を書く
QiitaPost post = new QiitaPost("タイトル", "本文");
//記事をアップロードする
QiitaServer.Upload(post);
//自分の記事にLGTMする!!
post.LGTM();

自分の記事に自分でLGTMできちゃいます。

なぜこのような問題が起きるのか?

なぜこのような問題が起きるのかというと、ズバリ「QiitaPostクラスにアクセスするための適切なインターフェイスが定義されていないから」に尽きると思います。

確かに、インターフェイスがなくてもQiitaPostクラス自体にはインスタンス名.メンバ名でアクセスできます。
アクセスできますというか、誰でも彼でもフリーでアクセスし放題です。

自販機で言えば、「売上金をすべて排出する」ボタンが客が触れられる場所に配置してあるようなものです。
普通の自販機は客用のインターフェイスと管理者用のインターフェイスが完全に分けられ、客は「売上金をすべて排出する」ボタンを押すことはできません。

それと同じで、クラスにも使用者に応じて適切なインターフェイスが定義されていないと、あとあと問題が発生することがあります。

これが、C#の「インターフェイス」の本質的な意味です。
…と僕は思います。

QiitaPostクラスにインターフェイスを定義してみる

では、実際にQiitaPostクラスのインターフェイスを定義して、先ほどの問題が発生しないようにしてみます。

記事を投稿する人には次の機能が必要でしょうか。

/// <summary>
/// 記事投稿者用のQiitaPostインターフェイス
/// </summary>
interface IAuthorQiitaPost
{
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount { get; }
    /// <summary>
    /// 記事を削除する
    /// </summary>
    void Delete();
}

記事を閲覧する人には次のようなインターフェイスを用意しました。

/// <summary>
/// 記事閲覧者用のQiitaPostインターフェイス
/// </summary>
interface IReaderQiitaPost
{
    /// <summary>
    /// 記事タイトルを取得する
    /// </summary>
    string Title { get; }
    /// <summary>
    /// 記事の本文を取得する
    /// </summary>
    string Text { get; }
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount { get; }
    /// <summary>
    /// 記事にLGTMする
    /// </summary>
    void LGTM();
    /// <summary>
    /// 記事をストックする
    /// </summary>
    void Stock();
}

おっと、ここで被っているメンバがありますね。
被っているメンバは更に抽象的なIQiitaPostインターフェイスにまとめてしまいましょう。

interface IQiitaPost
{
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount{ get; }
}

それに伴って、IAuthorQiitaPostインターフェイスとIReaderQiitaPostインターフェイスも次のように変更します。

/// <summary>
/// 記事投稿者用のQiitaPostインターフェイス
/// </summary>
interface IAuthorQiitaPost : IQiitaPost
{
    /// <summary>
    /// 記事を削除する
    /// </summary>
    void Delete();
}
/// <summary>
/// 記事閲覧者用のQiitaPostインターフェイス
/// </summary>
interface IReaderQiitaPost : IQiitaPost
{
    /// <summary>
    /// 記事タイトルを取得する
    /// </summary>
    string Title { get; }
    /// <summary>
    /// 記事の本文を取得する
    /// </summary>
    string Text { get; }
    /// <summary>
    /// 記事にLGTMする
    /// </summary>
    void LGTM();
    /// <summary>
    /// 記事をストックする
    /// </summary>
    void Stock();
}

とてもすっきりしました。

QiitaPostクラスにインターフェイスを実装する

では早速、作成したインターフェイスをQiitaPostクラスに実装させます。
といっても、すでに内部実装はされているのでクラス定義のところにインターフェイス名を書くだけですね。

class QiitaPost : IAuthorQiitaPost, IReaderQiitaPost
{
    //~略~
}

今回はインターフェイスに定義されたすべてのメンバが、すでにQiitaPostクラスに実装されているのでエラーが出ませんが、一つでも実装されていないメンバがあるとコンパイルエラーになります。
これが冒頭で説明した「インターフェイスは、実装するクラスにメソッドの実装を強制するものです」という説明に通じるわけですね。

QiitaPostクラスにインターフェイスを通じてアクセスさせる

それでは、インターフェイスを作成したので、「Qiitaの記事を作成する人」と「Qiitaの記事を読む人」にはQiitaPostクラスに直接アクセスするのをやめてもらい、きちんとインターフェイス経由でアクセスしてもらいましょう。

//記事を書く
IAuthorQiitaPost post = new QiitaPost("タイトル", "本文");
//記事をアップロードする
QiitaServer.Upload(post);
//自分の記事にLGTMできない〜〜〜
//post.LGTM();

postIAuthorQiitaPost型で定義していますので、自分で自分の記事にLGTMできません。
なぜなら、IAuthorQiitaPostインターフェイスのメンバに、LGTMメソッドが存在しないからです。

これで、記事を書いた人は自分でLGTMするとか余計なことができなくなり、おかしな使い方をされることはなくなりました。

続いて、記事を読む人にもインターフェイス経由で読んでもらいます。

//Qiita記事を取得
IReaderQiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

//勝手に人の書いた記事を消せない〜〜〜
//downloadedPost.Delete();

きちんと読む人専用のIReaderQiitaPostインターフェイス経由で読んでもらうことで、勝手に人の記事を消すなどという酷いことはできなくなりました。

このように、クラスを作るときには「誰に、どのように使ってほしいか」を意識した上で、適切にインターフェイスを用意することがとても重要です
適切なインターフェイスを用意し、使う側がきちんと然るべきインターフェイス経由でクラスにアクセスするようにすることで、想定外の使い方をされて不具合が発生することを防ぐことができます。

クラスを作る前にインターフェイスから作る

もっと言えば、クラスを作成する前にまずインターフェイスから設計することが好ましいと思います。

クラスの使い手と使い方を意識して、まずクラスのインターフェイスを作ります。
それが終わったあとに、クラスに実装させて、エラーが出なくなるまで内部の実装を書くという流れを心がけると、うっかりクラスに直接アクセスされてしまった!なんてことが少なくなると思います。

また、インターフェイスの設計は、クラス内部でどうやって実装しようかなどと考えることもなく、必要な機能をただ列挙していくだけなので、必要な機能を抜かりなく記述することができるというメリットもあります。

たとえば、今回の例だとIAuthorQiitaPostインターフェイスを設計するときに、「あれ、削除するだけじゃなくて編集する機能もいるな」と気づきやすくなると思います。

インターフェイスの「3タイプ」解説

さて、ここからが本題です。
タイトルで、「インターフェイスには目的に応じて3つのタイプがある」と書きました。
もう一度書くと次の3タイプです。

1. 疎結合を目的としたインターフェイス
2. クラスの機能を保証することを目的としたインターフェイス
3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

前項で扱ったインターフェイスは、このうちどれにあたるでしょうか?

もうおわかりですね、「3.クラスへの安全なアクセスを提供することを目的としたインターフェイス」にあたります。

では、他の2つはどんなインターフェイスなのかを解説していきます。

1. 疎結合を目的としたインターフェイス

1つ目は疎結合を目的として作られるインターフェイスです。
例えば次のようなものがあります。

//疎結合を目的として作られたインターフェイス
interface ITextReader
{
    string Read(string path);
} 
class TextReader : ITextReader
{
    public string Read(string path)
    {
        //テキストファイルを読み込んだ結果を返す処理
    }
}

この例のITestReaderインターフェイスは、疎結合を目的として作られたインターフェイスです。

利点1.クラスへの結合を弱くして変更に強くなる

image.png

使い手が、クラスに直接アクセスせずにインターフェイス経由でアクセスさせることで、クラス同士の結合度を下げることが目的です。
このようにしておくことで、たとえTextReaderクラスに変更が生じたとしても、TextReaderクラスがITextReaderインターフェイスを実装している限り、User側の変更は不要になります。

利点2.クラスへの結合が弱いので機能追加も容易になる

例えば、実際にテキストファイルを読み込むのではなく、デバッグ用に用意したダミーデータを読み込ませたいとします。
このとき、ITextReaderインターフェイスがあるおかげで、以下のようにすることができます。

image.png

ITextReaderインターフェイスを実装したDebugTextReaderクラス新たに登場しています。
しかし、ユーザー側はあくまでITextReaderインターフェイスにアクセスしています。

User側はただITextReaderだけを知っていて、その参照先が具体的にどのクラスなのかは知りませんから、これまたUser側の変更は不要になるのです。

このタイプのインターフェイスがないと変更がダイレクトに影響する

以下のように、インターフェイスを用意せずにクラス同士をダイレクトにアクセスさせると、UserクラスがTextReaderクラスの変更の影響をダイレクトに受けるようになります
image.png

例えば、TextReaderクラスのReadメソッドの名前がLoadに変わったとします。

class TextReader
{
    public string Load(string path)
    {
        //テキストファイルを読み込んだ結果を返す処理
    }
}

これだけで、TextReaderクラスを使っているUser側はReadLoadへの変更を余儀なくされます。
一つのクラスなら良いですが、これがたくさんのクラスから依存されていた場合、影響するすべてのコードを変更しなければなりません。

このようなことにならないよう、インターフェイスを用意しておくことで、TextReaderクラスは必ずITextReaderインターフェイスに準拠した実装にならなければなりません。
つまり、ITextReaderクラスを実装してさえいれば、ITextReaderインターフェイス経由でアクセスしている他のクラスへの影響はまったくなくなるのです。

どうでしょう。このタイプのインターフェイスの利点がおわかりいただけたでしょうか。

理想は1クラスにつき少なくとも1インターフェイス

どのようなクラスにも必ずアクセス用のインターフェイスを用意しておくことが理想と思います。
面倒と思わずに、インターフェイスを用意しておくだけで、万が一の変更があったときに大いに役立ってくれるでしょう。

2. クラスの機能を保証することを目的としたインターフェイス

続いて、「クラスの機能を保証することを目的としたインターフェイス」について説明します。

このタイプのインターフェイスは、記事の序盤で「よくあるインターフェイスの説明」として挙げた「インターフェイスは、実装するクラスにメソッドの実装を強制するもの」という説明を受けたとしても最も納得しやすいタイプです。

要は、インターフェイスを実装したクラスは、そのインターフェイスに定義されたメソッドは必ず実装されるのだから、特定の機能があることが保証されますよ、ということですね。

このタイプのインターフェイスとして、有名なものがいくつかありますので列挙します。

インターフェイス 保証する機能
IEnumerable foreachで回すことができる
IEqautable 値の等価性を評価することができる
IDisposable 明示的にメモリを開放することができる
IObservable クラスからの通知を受け取ることができる

例えば、IEnumerable<T>インターフェイスを実装したクラスは、IEnumerator<T>を返すGetEnumerator()メソッドの実装を強制されるので、foreachステートメントで回すことができることが保証されます。
IEquatable<T>インターフェイスを実装したクラスは、bool Equals(T other)メソッドの実装を強制されるので、他のオブジェクトとの等価性を比較できることが保証されます。

このように、クラスに一定の機能があることを保証するために使われるインターフェイスが、このタイプです。

また、クラスの使い手側も、「あ、このクラスはIEnumerableだからforeachで回せるな」「このクラスはIDisposableだから使い終わったらDisposeしなくちゃいけないんだな」などと、クラスの定義を見ただけでそのクラスの性質を簡単に理解できることも利点の一つですね。

もちろんポリモーフィズムによる利点も

もちろん、利点はそれだけではなく、ポリモーフィズムを利用した利点もあります。

例えばIEnumerable<T>インターフェイスは、List<T>, Dictionary<TKey, TValue>, T[]など、様々なクラスが実装しているので、IEnumerable<T>型の変数には、それを実装した様々なクラスを受けることができます。

例えばメソッドの引数として、IEnumerableで受けておけば、使う側はそこに代入できるインスタンスの選択肢が大幅に増えるわけです。
逆に、メソッドの引数をListなどの具象クラスにしてしまうと、使う側はListのインスタンスしか代入できなくなってしまいます。1

このように、ポリモーフィズムによるメリットも享受することができます。

3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

最後に、記事中盤でも解説した「クラスへの安全なアクセスを提供することを目的としたインターフェイス」です。

このインターフェイスの利点はもうお分かりいただけたと思うので、このタイプで有名なインターフェイスを紹介します。

インターフェイス 提供するアクセス
IReadOnlyList Listを読み取り専用で提供する
IReadOnlyCollection IReadOnlyListから更にインデクサによるアクセスを削除
IReadOnlyReactiveProperty 購読と値の読み取りだけができるReactiveProperty

見ての通り、ただ単にアクセスを制限させるものばかりですね。
しかし、これが非常に重要な役割を持ちます。

Listをそのまま渡すのではなく、IReadOnlyListとして渡すだけで、渡した先で勝手に書き換えられる危険性が皆無2になりますから、これを使わない手はありません。
詳しくは下記記事で詳しく解説されているので、参考にしてください。自分も大変お世話になった記事です。
https://qiita.com/lobin-z0x50/items/248db6d0629c7abe47dd

最後に

以上、「インターフェイス」の本質的な意味と、それからインターフェイスの3つのタイプを解説しました。

すごく長くなってしまいましたが、一口に「インターフェイス」と言っても、目的に応じて3つのタイプが有ることをご理解いただけたでしょうか。
インターフェイスの利点があまり良くわかっていない方も、「インターフェイスには3つのタイプがある」ことを意識するだけで、ぐっと理解度が深まると思います。

ただ、冒頭でも書きましたがこの記事の内容は一個人の持論です。
もし説明がおかしい、間違っている等の他、ご意見ご感想などありましたら、ぜひぜひコメントください。すごく喜んで返信します。


  1. もちろん、メソッド内でListの固有メソッドを使う場合は素直にListで受けるべきです。 

  2. [2020/09/10追記]@htsign さんにご指摘いただきました。IReadOnlyListとして渡しても、IListにキャストされてしまえば普通に追加削除可能なので、危険性が「皆無」というわけではないようです。ただ、IReadOnlyListとあからさまに「リードオンリーなリスト」をわざわざ書き換え可能となるようにキャストするというのは、あまり考えられない行為と思いますので、IReadOnlyListとして渡す有用性は十分にあると感じます。もし本当に書き換えられるのを阻止したい場合、ImmutableListもしくはReadOnlyCollectionを使えば実現可能です。 

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