アジョブジ星通信

進捗が出た頃に更新されるブログ。

TargetFrameworksからビルドできそうなフレームワークだけビルドする

これから1ヶ月以上ブログ書いてなかった。というわけで、これの続き。

この記事では PCL や Xamarin 用のアセンブリdotnet build でビルドできるようにしましたが、これだと MS の不自由アセンブリや Xamarin がインストールされていないとエラーになってしまうので、なんとかして警告レベルに落としたいので、やっていきます。

まずは対処前の状況。こんな感じのプロジェクトをビルドしていきます。

<TargetFrameworks>netstandard1.1;netstandard1.5;portable45-net45+win8+wpa81;monoandroid10;monotouch10;xamarinios10</TargetFrameworks>

https://github.com/CoreTweet/CoreTweet/blob/1d53cb7888eda55e681ed865538d95c0eb394da8/CoreTweet/CoreTweet.csproj

するとこう。

PCL がコケたところでビルドが止まってしまっていますね。というか並列ビルドしてくれよ。

では、 TargetFrameworks からビルドできなさそうなフレームワークを消していくコードを追加していきましょう。やり方を言葉で表すとこんな感じ。

  1. まとめてビルドするやつ(DispatchToInnerBuilds ターゲット)の実行前にフック
  2. 次の手順をすべてのフレームワークに実行(といっても netstandard や netcoreapp は絶対ビルドできるし、次の手順には失敗するので素通りさせる)
    1. GetReferenceAssemblyPaths タスクを叩いてみる
    2. 結果が空でないなら TargetFrameworks に追加する

以上を全フレームワークに実行します。

というわけで実際のコードはこちら。

<Target Name="CheckAvailableFrameworks" BeforeTargets="DispatchToInnerBuilds">
  <ItemGroup>
    <!-- _TargetFramework という名前は DispatchToInnerBuilds 内で使われているので、被らないようにアンダーバー増やした -->
    <__TargetFramework Include="$(TargetFrameworks)" />
    <!-- netcoreapp もあるならそれもちゃんと Condition に書きましょう -->
    <_OldTargetFramework Include="@(__TargetFramework)" Condition="!$([System.String]::Copy('%(Identity)').StartsWith('netstandard'))" />
    <_NewTargetFramework Include="@(__TargetFramework)" Condition="$([System.String]::Copy('%(Identity)').StartsWith('netstandard'))" />
  </ItemGroup>
  <!-- TargetFramework プロパティをセットして CheckCompilability を呼び出す -->
  <!-- TargetFramework プロパティがセットされていると、うまいこと TargetFrameworkMoniker プロパティがセットされるのを利用していきたい -->
  <MSBuild Projects="$(MSBuildProjectFile)"
           Targets="CheckCompilability"
           Properties="TargetFramework=%(_OldTargetFramework.Identity)">
    <!-- 結果をとりあえずアイテム化しておいて -->
    <Output TaskParameter="TargetOutputs" ItemName="_NewTargetFramework" />
  </MSBuild>
  <PropertyGroup>
    <!-- 強引にプロパティに変換する -->
    <TargetFrameworks>@(_NewTargetFramework)</TargetFrameworks>
  </PropertyGroup>
</Target>

<Target Name="CheckCompilability" Returns="$(CompilableFramework)">
  <!-- ContinueOnError="true" によってエラーを警告化 -->
  <!-- これで完全勝利 -->
  <GetReferenceAssemblyPaths TargetFrameworkMoniker="$(TargetFrameworkMoniker)"
                             RootPath="$(TargetFrameworkRootPath)"
                             ContinueOnError="true">
    <Output TaskParameter="ReferenceAssemblyPaths" PropertyName="_ReferenceAssemblyPaths" />
  </GetReferenceAssemblyPaths>
  <PropertyGroup Condition="'$(_ReferenceAssemblyPaths)' != ''">
    <!-- 成功したときだけ Returns に追加されるってわけ -->
    <CompilableFramework>$(TargetFramework)</CompilableFramework>
  </PropertyGroup>
</Target>

これで警告になりました。めでたしめでたし。

新しい csproj に負けないレガシーな心を持ち続けて云々

Sdk="Microsoft.NET.Sdk" なプロジェクトファイルで使える小ネタ集です。

NETStandard.Library への参照をいじる

PackageReference を何ひとつ書かなくても NETStandard.Library を勝手にダウンロードしてくるのは、手軽にコードが書けるという点ではいいかもしれないけれど、やはり無駄なパッケージ参照があるというのは許せないものなので、消していきましょう。

DisableImplicitFrameworkReferences というプロパティがあり、これを true にするとデフォルトの参照が消えます。デフォルトの参照は以下の通り(targets ファイルを漁るとでてくる)で、これらが全部消えます。つまり .NET Framework なら mscorlib だけ、 .NET Core なら無です。

  • .NETStandard (Microsoft.NET.Sdk.DefaultItems.props)
    • NETStandard.Library 1.6.1
  • .NETCoreApp (Microsoft.NET.Sdk.DefaultItems.props)
  • .NETFramework (Microsoft.NET.Sdk.BeforeCommon.targets)
    • System
    • System.Data
    • System.Drawing (マジかよ)
    • System.Xml
    • System.Core
    • System.Runtime.Serialization
    • System.Xml.Linq
    • System.Numerics
    • System.IO.Compression.FileSystem

NETStandard.Library のバージョンは NetStandardImplicitPackageVersion プロパティを、 Microsoft.NETCore.App のバージョンは RuntimeFrameworkVersion プロパティを書き換えることで、自由に変更することができます。

netcoreapp1.0 をターゲットにすると Microsoft.NETCore.App のバージョンは 1.0.4 になるけど、 1.1.1 に変えても問題なく動くんだよな……。

サンプルコードとして、 NETStandard.Library を消して、 System.Console (とその依存パッケージ)だけを参照するプロジェクトです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard1.4</TargetFramework>
    <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Console" Version="4.3.0" />
  </ItemGroup>

</Project>

Portable Class Library や Xamarin クラスライブラリをビルドする

プロジェクトファイルにちょっと手を加えれば、インストールされているフレームワークなら何でもビルドできます。たぶん。

TargetFramework, TargetFrameworkIdentifier, TargetFrameworkVersion、あとプロファイル指定が必要なら TargetFrameworkProfile というプロパティを設定すれば、勝手にアセンブリ解決してビルドしてくれます。

例えば、いにしえのフレームワーク軍を相手する portable40-net40+sl5+win8+wp8+wpa81 ならこんな感じ

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>portable40-net40+sl5+win8+wp8+wpa81</TargetFramework>
    <TargetFrameworkIdentifier>.NETPortable</TargetFrameworkIdentifier>
    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
    <TargetFrameworkProfile>Profile328</TargetFrameworkProfile>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="System" />
  </ItemGroup>

</Project>

mscorlib 以外は勝手に参照されないので、必要に応じて Reference を書いてあげる必要があります。

Xamarin も同様にできますが、ファサードアセンブリを使うために ImplicitlyExpandDesignTimeFacades プロパティを true にしておく必要があるのと、 Visual Studio 2017 からアセンブリの場所が変わったので、それに対する対応を入れる必要があります。

Visual Studio 2015 までは C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework に Xamarin のアセンブリがありましたが、 2017 では C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\ReferenceAssemblies\Microsoft\Framework になりました。 Microsoft.NET.Sdk では前者のパスを探しにいくので、 2017 だけがインストールされている環境ではアセンブリが見つかりません。

その対策が入っている Xamarin.Android.Sdk.props をパクりつつ、 Xamarin クラスライブラリをつくるプロジェクトファイルはこんな感じになります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>monoandroid70</TargetFramework>
    <TargetFrameworkIdentifier>MonoAndroid</TargetFrameworkIdentifier>
    <TargetFrameworkVersion>v7.0</TargetFrameworkVersion>
    <TargetFrameworkRootPath Condition="'$(VsInstallRoot)' != ''">$(VsInstallRoot)\Common7\IDE\ReferenceAssemblies\Microsoft\Framework\</TargetFrameworkRootPath>
    <ImplicitlyExpandDesignTimeFacades>true</ImplicitlyExpandDesignTimeFacades>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="Mono.Android" />
  </ItemGroup>

</Project>

Xamarin Android プロジェクトで、このプロジェクトを参照に加えて実行してみましたが、問題なく動作しました。

VsInstallRoot プロパティは Visual Studio についている MSBuild でしか設定されていないので、 .NET Core SDKMSBuild でビルドした場合には 2015 までのアセンブリの場所を見に行きます。

MySQL Connector/.NET の TreatTinyAsBoolean のバグと Dapper

MySQL Connector/.NET、長いので MySql.Data と呼びます。

バグについて

MySql.Data の接続文字列で使用できるオプションとして、「TreatTinyAsBoolean」または「Treat Tiny As Boolean」というものがあります。これはデフォルトで有効になっていて、このオプションが有効のときは TINYINT(1) のカラムを Boolean 型として扱います。

で、常に Boolean として扱ってくれるかというとそうではないという問題があります。 TINYINT(1) 型のカラムに null が入っていると、それ以降のレコードは Boolean ではなく SByte になります。これによってありがた迷惑な機能から、ただの迷惑な機能に昇格しましたね。おめでとう。

続きを読む

自分のスキルセットを確認してみる

シャワー浴びながらふと気づいたこと、自分には何ができるのだろうか。いつも自己紹介が苦手といってきたけれど、いい加減自分をアピールできる材料くらい知っておかないと思いリストアップしてみようと思います。

続きを読む

書きはじめ

あけましたね、おめでとうございます。2016年の振り返りと2017年の抱負を豊富に書くのが恒例行事らしいのでやるか。知らんけど。AbemaTV ではっぴぃにゅうにゃあしながら書いてるので自分でも何書いてるかわからなくなりそう。

2016年の振り返り

去年のことは去年のうちに書き捨てておくべきでしたが、ソフィーのアトリエやってガキの使い見てたら年が変わってたので許してほしい。あと記憶力ゼロ太郎なので覚えてない。

大学に入学した

北千住駅の目の前のオタクが詰め込まれているあそこです。レポートを手書きさせられています。

勉強意欲のほうですが、中学生のころ並には回復してきました。高校生のときは受験受験いわれて完全にやる気を失っていたのでね、多少マシになった(勉強したいとは言ってない)。

あと帰宅部コミュ障なのでお友達が少ない。ほかに書くことないのかボケ。

アルバイ

正直クソツイート太郎なので、僕のツイアカウントを某社リストに入れられるのつらいんですけど、って感じには隠れていたいんですけど、ついにコード書いてお金がもらえるようになりました。こういうことしてます。

仕事力(いろいろな要素があります)高いマンを見てると、人間力がほしい~って気持ちになりますね(語彙力)。とりあえず役には立てているらしいので、精進していきます。

ほかなにしたっけ

パフォーマンスやくざになったり、 null 撲滅過激派になったりした記憶があります。みなさんも Rust を書いて安全なプログラミングについて考えましょう、という感じです。

ほんと日本語力も記憶力もねーなこいつ。

2017年だよ

今週のお題「2017年にやりたいこと」

宣言なんてするもんじゃなかった。

いつも通り C# 界隈の情報を追いつつ、 Rust 界隈の情報も得ていく活動を続けながら、大学で新しい技術を身に着けていきたいなぁといったところです。今年もよろしくお願いします。

CoreFXで進化したLINQのお話

昔々のお話

こんなライブラリを作った記憶があったのですが、最近 C# パフォーマンスヤクザ[要出典]になりかけている僕に、 IReadOnlyCollection<T> を使用することで、 リスト→LINQ→ToArray といった処理を効率化できるのではないだろうかと考えて、このライブラリをちゃんと書き直すぞ!と考えていた矢先、 CoreFXSystem.Linq.Enumerable が進化していることに気づいたのでまとめておきます。

なお、ここで紹介する内容は、 .NET Core 1.0 に含まれており、 .NET Core App で使うことができます。 .NET Standard 1.6 以上である必要があるので、 .NET Framework のほうで使えるようになるのは 4.6.3 になると思います。先が長い。

追記: .NET Framework 4.7.1 では IListProvider だけ導入されました。とはいえ、配列や List に対する最適化が入っていないので、変化はないでしょう。 IPartition は最後の例が示すように互換性に問題があるので .NET Framework には導入されないかもしれません。

追加されたメソッド

  • Append
  • Prepend

Append は最後に 1 要素追加、 Prepend は最初に 1 要素を追加するメソッドです。こんな感じに使えます。

var e = new[] { 1 }.Prepend(0).Append(2);
foreach (var x in e)
    Console.WriteLine(x);
// 0
// 1
// 2

これで .Concat(new[] { hoge }) とか書かなくてよくなりますね。やった。

最適化のお話

ではメインのお話行きましょう。今まで LINQ は「書きやすいけど遅い」という問題が付き物でした。原因は主に以下の 2 つだと考えています。

  • IEnumerator<T> を愚直にラップしまくっていく挙動をするので、1回 MoveNext を呼び出すだけで深いコールスタックが生まれる(しかもインターフェイスメソッド)
  • array.Select(x => ...).ToArray() のような出力される配列の長さが明らかな操作であっても、長さ 4 の配列を確保して足りなくなったら 2 倍して…という挙動をする

今までも対策がされていなかったわけではなく、1については、 WhereSelect はよく使われるので、専用の Iterator が用意されていましたし、2については ICollection<T> ならば Count プロパティを使うということをしていました。とはいえこれだけでは不十分なのは2で例に挙げたものからも明らかだと思います。

そこで、新たに導入されたのが、 IIListProviderIPartition です。まずは定義を見てみましょう。

// Copyright (c) .NET Foundation and Contributors

/// <summary>
/// An iterator that can produce an array or <see cref="List{TElement}"/> through an optimized path.
/// </summary>
internal interface IIListProvider<TElement> : IEnumerable<TElement>
{
    /// <summary>
    /// Produce an array of the sequence through an optimized path.
    /// </summary>
    /// <returns>The array.</returns>
    TElement[] ToArray();

    /// <summary>
    /// Produce a <see cref="List{TElement}"/> of the sequence through an optimized path.
    /// </summary>
    /// <returns>The <see cref="List{TElement}"/>.</returns>
    List<TElement> ToList();

    /// <summary>
    /// Returns the count of elements in the sequence.
    /// </summary>
    /// <param name="onlyIfCheap">If true then the count should only be calculated if doing
    /// so is quick (sure or likely to be constant time), otherwise -1 should be returned.</param>
    /// <returns>The number of elements.</returns>
    int GetCount(bool onlyIfCheap);
}

internal interface IPartition<TElement> : IIListProvider<TElement>
{
    IPartition<TElement> Skip(int count);

    IPartition<TElement> Take(int count);

    TElement TryGetElementAt(int index, out bool found);

    TElement TryGetFirst(out bool found);

    TElement TryGetLast(out bool found);
}

corefx/Partition.cs at release/1.1.0 · dotnet/corefx · GitHub

このインターフェイスを実装しているイテレーターは、インターフェイスのメンバーにある処理が普通にループを回すのに比べて省略可能だということを表します。これによって操作が最適化され、可能な場合には ElementAt, Last が O(1) になったり、 ToArrayToList で配列のアロケーション回数を減らしたりできます。

簡単な例を紹介します。

var e = Enumerable.Range(0, 100)
    .Select(x =>
    {
        Console.WriteLine(x);
        return x;
    })
    .Skip(95);

foreach (var _ in e);

このコードを .NET Framework 4.6.2 で実行すると、 0 から 99 までコンソールに出力されますが、 .NET Core で実行すると 95 から 99 までしか出力されません。これは、 RangeIteratorIPartition を実装しており、 SelectSelectIPartitionIterator が使用され、うまく Skip を省略することに成功しています。 IPartitionRange のほか、 RepeatOrderBy の結果が実装しています。もちろん IList<T>Select, Skip, Take を実行した場合にも IPartition を実装した戻り値が生成されます。

また、 IIListProvider 単体は、 DistinctUnion など、すべて Set に突っ込んだ結果を配列に移す操作がショートカットできる場所や、 Append, Prepend, Concat のように ToList を効率化できる場所に使用されています。

まとめ

Append, Prepend, ConcatIPartition を特別扱いしないのが気に食わないですが、とにかく、何も考えずに LINQ を使っても圧倒的無駄な処理にならずに済むようになるのはとても大きいと思います。というわけで早く .NET Framework 4.6.3 出してくれ。

取り急ぎ古いNuGetでもインストールできるパッケージをつくる

CoreTweet 0.7.0.339 を NuGet 2.x 系を使う環境(Visual Studio 2013 でアップデート入れてない or MonoDevelop/Xamarin Studio 6.0.x以下)でインストールしようとすると、こんな感じのエラーが出ることは知ってましたがリリースしてしまいました、が思ったより影響大きかった(エゴサ調べ)ので、なんとかしたお話。

f:id:azyobuzin:20160811000149p:plain

原因

0.7.0.339 の nuspec ファイルの依存パッケージが記述されている部分がこちら。

<dependencies>
    <group targetFramework="net35">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="portable-win81+wpa81">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="portable-net45+dnxcore50+win8+wpa81+MonoAndroid+xamarinios+MonoTouch">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="monoandroid">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="monotouch">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="xamarinios">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="netstandard1.1">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
        <dependency id="System.Dynamic.Runtime" version="4.0.11" />
        <dependency id="System.Globalization" version="4.0.11" />
        <dependency id="System.Linq" version="4.1.0" />
        <dependency id="System.Linq.Expressions" version="4.1.0" />
        <dependency id="System.Net.Http" version="4.1.0" />
        <dependency id="System.Reflection.Extensions" version="4.0.1" />
        <dependency id="System.Runtime.Extensions" version="4.1.0" />
        <dependency id="System.Text.RegularExpressions" version="4.1.0" />
        <dependency id="System.Threading" version="4.0.11" />
    </group>
    <group targetFramework="netcoreapp1.0">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
        <dependency id="System.Dynamic.Runtime" version="4.0.11" />
        <dependency id="System.Globalization" version="4.0.11" />
        <dependency id="System.IO.FileSystem" version="4.0.1" />
        <dependency id="System.Linq" version="4.1.0" />
        <dependency id="System.Linq.Expressions" version="4.1.0" />
        <dependency id="System.Net.Http" version="4.1.0" />
        <dependency id="System.Reflection.Extensions" version="4.0.1" />
        <dependency id="System.Runtime.Extensions" version="4.1.0" />
        <dependency id="System.Security.Cryptography.Algorithms" version="4.2.0" />
        <dependency id="System.Text.RegularExpressions" version="4.1.0" />
        <dependency id="System.Threading" version="4.0.11" />
    </group>
</dependencies>

この中で、 NuGet 2.x 系で対応していないフレームワークは netstandard1.1 と netcoreapp1.0 の2つです。さて、 NuGet 内部では対応していないフレームワークはまとめて「Unsupported 0.0」として扱われます。つまりこういう状況。

<dependencies>
    <group targetFramework="net35">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="portable-win81+wpa81">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="portable-net45+dnxcore50+win8+wpa81+MonoAndroid+xamarinios+MonoTouch">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="monoandroid">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="monotouch">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="xamarinios">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
    </group>
    <group targetFramework="Unsupported,Version=0.0">
        <dependency id="Newtonsoft.Json" version="9.0.1" />
        <dependency id="System.Dynamic.Runtime" version="4.0.11" />
        <dependency id="System.Globalization" version="4.0.11" />
        <dependency id="System.Linq" version="4.1.0" />
        <dependency id="System.Linq.Expressions" version="4.1.0" />
        <dependency id="System.Net.Http" version="4.1.0" />
        <dependency id="System.Reflection.Extensions" version="4.0.1" />
        <dependency id="System.Runtime.Extensions" version="4.1.0" />
        <dependency id="System.Text.RegularExpressions" version="4.1.0" />
        <dependency id="System.Threading" version="4.0.11" />

        <dependency id="Newtonsoft.Json" version="9.0.1" />
        <dependency id="System.Dynamic.Runtime" version="4.0.11" />
        <dependency id="System.Globalization" version="4.0.11" />
        <dependency id="System.IO.FileSystem" version="4.0.1" />
        <dependency id="System.Linq" version="4.1.0" />
        <dependency id="System.Linq.Expressions" version="4.1.0" />
        <dependency id="System.Net.Http" version="4.1.0" />
        <dependency id="System.Reflection.Extensions" version="4.0.1" />
        <dependency id="System.Runtime.Extensions" version="4.1.0" />
        <dependency id="System.Security.Cryptography.Algorithms" version="4.2.0" />
        <dependency id="System.Text.RegularExpressions" version="4.1.0" />
        <dependency id="System.Threading" version="4.0.11" />
    </group>
</dependencies>

こうすると Unsupported で2個同じパッケージが含まれているので、検証で殺されて、スクリーンショットのエラーメッセージが出るようになるわけです。

この問題は、 netstandard, netcoreapp に対応した 2.11 で解決しています。

どう対処する?

A. 諦めるか、古い NuGet が非対応のフレームワークを1つまでにする

とりあえず CoreTweet 0.7.1.345 では依存パッケージの宣言から netcoreapp を消し去りました(消し去るために少しコードもいじりましたが)。

というわけで、なかなか厳しい状況なので、早く NuGet 3 系が広まって欲しい。 Xamarin Studio 6.1 はよ。