LoginSignup
1
2

More than 3 years have passed since last update.

[Roslyn] オブジェクトインスタンス化コード生成に必要な要素の調査(おまけ)

Last updated at Posted at 2020-05-26

前書き

オブジェクトインスタンス化コード生成に必要な要素の調査で、オブジェクトインスタンス化に必要な最低限の要素を取得したが、せっかくなので取得結果を用いて、実際にインスタンス化のコード生成をユニットテストを書いて確認したので投下しておく。

確認内容

生成するコードとして、

  • Nullable + コンストラクタをもつ構造体
  • プロパティでの初期化が必要な構造体
  • IEnumerable + 公開フィールドでの初期化が必要な構造体

の3パターンについて、これらの型を戻りとするメソッドを持つインターフェースでもって確認した。

意味解析の結果を取得する際、ASTを構築する必要があるため、そのあたりの諸々ののコードを静的クラスSyntaxGeneratorHelperとして用意した(Roslynによるインターフェースの実装クラスの構築で作ったコードを叩き台にして切り出しただけ)。

    public static class SyntaxGeneratorHelper {
        public static UsingDirectiveSyntax ToUsingDirective(string inUsing) {
            return 
                SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(inUsing).WithLeadingTrivia(SyntaxFactory.Space))
                .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
            ;
        }
        public static CSharpCompilation CreateCompilation(SyntaxTree inDaoAST, SyntaxTree inEntityAST) {
            var dotnetCoreDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

            var opts = new CSharpCompilationOptions(
                outputKind: OutputKind.DynamicallyLinkedLibrary
            );

            return 
                CSharpCompilation.Create("autoGen", 
                    syntaxTrees: new[] { 
                        inDaoAST, inEntityAST                  
                    },
                    references: new[] {
                        AssemblyMetadata.CreateFromFile(typeof(object).Assembly.Location).GetReference(),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "netstandard.dll")),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "System.Runtime.dll")),
                    },
                    options: opts
                );
        }    
    }
}

コード生成についてのユニットテスト

以下ソースコード生成を行うソースコード

using NUnit.Framework;

using System;
using System.IO;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using GenSyntaxTestHelpers;
using SemSymbols;

namespace GenTypeInitializationTests
{
    public class GenTypeInitializationTest {
        [SetUp]
        public void Setup()
        {
        }

        private static readonly string IntfSource1 = @"
        namespace SemModels {
            public interface IColorDao1 {
                ColorData? FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource1 = @"
        namespace SemModels {
            public readonly struct ColorData {
                public int Id { get; }
                public string Name { get; }
                public int Red { get; }
                public int Green { get; }
                public int Blue { get; }

                public ColorData(int id, string name, int red = default, int green = default, int blue = default) => 
                    (Id, Name, Red, Green, Blue) = (id, name, red, green, blue);
            }    
        }    
        ";

        private static readonly string IntfSource2 = @"
        namespace SemModels {
            public interface IColorDao2 {
                ColorDataMut FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource2 = @"
        namespace SemModels {
            public struct ColorDataMut {
                public int Id { get; set; }
                public int Code { get => this.Id; }
                public string Name { get; set; }
                public int Red { get; set; }
                public int Green { get; set; }
                public int Blue { get; set; }
            }    
        }    
        ";

        private static readonly string IntfSource3 = @"
        using System.Collections.Generic;

        namespace SemModels {
            public interface IColorDao3 {
                IEnumerable<ColorDataMut2> FindAll();
            }
        }            
        ";

        private static readonly string EntitySource3 = @"
        namespace SemModels {
            public struct ColorDataMut2 {
                public int id;
                public string name;
                public int red;
                public int green;
                public int blue;
            }    
        }    
        ";

        [Test]
        public void _メソッドの実装_Nullableな戻り値_コンストラクタが適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource1);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource1);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl: IColorDao1 {

                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);

            var parameters = ctx.Constructors[0].Symbols;
            var arg = method.ParameterList.Parameters[0].Identifier.Text;

            var implMethod = 
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                        SyntaxFactory.ReturnStatement(
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList<ArgumentSyntax>(new[] {
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[0].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.IdentifierName(arg)
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[1].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("cyan"))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[2].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[3].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[4].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                        ),
                                    })),
                                    null

                                )
                                .WithLeadingTrivia(SyntaxFactory.Space)
                        )
                    )
                )
            ;

            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("ConstructorEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(1), "生成されたクラス数");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl"), "生成された型名"); 

                var instance = (SemModels.IColorDao1)asm.CreateInstance("SemModelsImpl.ColorDaoImpl");

                var data = instance.FindById(4);

                Assert.That(data, Is.InstanceOf<System.Nullable<SemModels.ColorData>>(), "目的の値が生成されていること");
                Assert.That(data.HasValue, Is.True, "内包する型のインスタンスが生成されていること"); 
                Assert.That(data.Value.Id, Is.EqualTo(4), "渡したIdと一致していること");
                Assert.That(data.Value.Name, Is.EqualTo("cyan"), "色名");
                Assert.That(data.Value.Red, Is.EqualTo(0), "赤成分");
                Assert.That(data.Value.Green, Is.EqualTo(255), "緑成分");
                Assert.That(data.Value.Blue, Is.EqualTo(255), "青成分");
            });

            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }

        [Test]
        public void _メソッドの実装_Nullableな戻り値_コプロパティによる初期化が適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource2);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource2);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl2: IColorDao2 {

                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);

            var props = ctx.PropertyTypeVars.Symbols;
            var arg = method.ParameterList.Parameters[0].Identifier.Text;

            var implMethod =
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(),
                                    SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression,
                                        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[0].Name),
                                                SyntaxFactory.IdentifierName(arg)
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[1].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("magenta"))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[2].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[3].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[4].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            )
                                        })
                                    )
                                )
                                .WithLeadingTrivia(SyntaxFactory.Space)
                            )
                    )
                )
            ;


            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("PropertyEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(1), "生成されたクラス数");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl2"), "生成された型名"); 

                var instance = (SemModels.IColorDao2)asm.CreateInstance("SemModelsImpl.ColorDaoImpl2");

                var data = instance.FindById(5);

                Assert.That(data, Is.InstanceOf<SemModels.ColorDataMut>(), "目的の値が生成されていること");
                Assert.That(data.Id, Is.EqualTo(5), "渡したIdと一致していること");
                Assert.That(data.Name, Is.EqualTo("magenta"), "色名");
                Assert.That(data.Red, Is.EqualTo(255), "赤成分");
                Assert.That(data.Green, Is.EqualTo(0), "緑成分");
                Assert.That(data.Blue, Is.EqualTo(255), "青成分");            
            });

            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }


        [Test]
        public void _メソッドの実装_Nullableな戻り値_公開フィールドによる初期化が適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource3);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource3);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);

            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl3: IColorDao3 {

                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);

            var fields = ctx.FieldTypeVars.Symbols;

            var implMethod =
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                            SyntaxFactory.YieldStatement(
                                SyntaxKind.YieldReturnStatement,
                                SyntaxFactory.Token(SyntaxKind.YieldKeyword),
                                SyntaxFactory.Token(SyntaxKind.ReturnKeyword).WithLeadingTrivia(SyntaxFactory.Space),                                
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(),
                                    SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression,
                                        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[0].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(5))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[1].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("yellow"))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[2].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[3].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[4].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                            )
                                        })
                                    )
                                )
                                .WithLeadingTrivia(SyntaxFactory.Space),
                                SyntaxFactory.Token(SyntaxKind.SemicolonToken)
                            )
                    )
                )
            ;

            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "System.Collections.Generic",
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("FieldEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(2), "生成されたクラス数(自動生成されたIteratorを含むため)");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl3"), "生成された型名"); 

                var instance = (SemModels.IColorDao3)asm.CreateInstance("SemModelsImpl.ColorDaoImpl3");

                var seq = instance.FindAll();

                Assert.That(seq, Is.InstanceOf<IEnumerable<SemModels.ColorDataMut2>>(), "目的の値が生成されていること");
                Assert.That(seq.Any(), Is.True, "内包する型のインスタンスが生成されていること");
                Assert.That(seq.Count(), Is.EqualTo(1), "要素数");

                var data = seq.First();

                Assert.That(data.id, Is.EqualTo(5), "渡したIdと一致していること");
                Assert.That(data.name, Is.EqualTo("yellow"), "色名");
                Assert.That(data.red, Is.EqualTo(255), "赤成分");
                Assert.That(data.green, Is.EqualTo(255), "緑成分");
                Assert.That(data.blue, Is.EqualTo(0), "青成分");            
            });

            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }

        private EmitResult EmitGenUnit(string inAsmName, CompilationUnitSyntax inGenTree, Action<Assembly> inCallback) {
            using(var stream = new MemoryStream()) {
                var dotnetCoreDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

                var opts = new CSharpCompilationOptions(
                    outputKind: OutputKind.DynamicallyLinkedLibrary
                );

                var newCompilation = CSharpCompilation.Create(inAsmName, 
                    syntaxTrees: new[] { SyntaxFactory.SyntaxTree(inGenTree) },
                    references: new[] {
                        AssemblyMetadata.CreateFromFile(typeof(object).Assembly.Location).GetReference(),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "netstandard.dll")),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "System.Runtime.dll")),
                        AssemblyMetadata.CreateFromFile(this.GetType().Assembly.Location).GetReference(),
                    },
                    options: opts
                );

                var emitResult = newCompilation.Emit(stream);

                if (emitResult.Success) {
                    Assert.That(inCallback, Is.Not.Null);

                    stream.Position = 0;
                    var buf = new byte[stream.Length];
                    stream.Read(buf, 0, buf.Length);

                    var asm = Assembly.Load(buf);

                    inCallback(asm);
                }

                return emitResult;
            }
        }
    }
}

namespace SemModels {
    public readonly struct ColorData {
        public int Id { get; }
        public string Name { get; }
        public int Red { get; }
        public int Green { get; }
        public int Blue { get; }

        public ColorData(int id, string name, int red = default, int green = default, int blue = default) => 
            (Id, Name, Red, Green, Blue) = (id, name, red, green, blue);
    }    

    public interface IColorDao1 {
        ColorData? FindById(int id);
    }

    public struct ColorDataMut {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Red { get; set; }
        public int Green { get; set; }
        public int Blue { get; set; }
    }    

    public interface IColorDao2 {
        ColorDataMut FindById(int id);
    }

    public struct ColorDataMut2 {
        public int id;
        public string name;
        public int red;
        public int green;
        public int blue;
    }    

    public interface IColorDao3 {
        IEnumerable<ColorDataMut2> FindAll();
    }
}

追記

テストコードにコメントアウトしたゴミを拾い集めコード生成の手引きとしてまとめる

Roslyn@SyntaxFactory`はアホほどメソッドが生えててどれを使ったいいか結構迷う。

その際の手引きとして、

var body = SyntaxFactory.ParseCompilationUnit("public SemModels.ColorData x(int id) { return new SemModels.ColorData() { Red = 128 }; }");
foreach (var n in body.DescendantNodesAndTokensAndSelf()) {
   TestContext.Progress.WriteLine($"{n.Kind().ToString().PadRight(30)}{n.ToFullString()}");
}

のように文字列から構文木を実体化させ、コンソールに出力してみるのが近道かも。
(NUnitの場合、TestContext.Progress.WriteLineメソッドで、出力してくれる)

また、組み立てた構文木についても、ToFullString()メソッドを呼ぶことで文字列に戻すことができる(インデントはめちゃくちゃな可能性あり)

TestContext.Progress.WriteLine(newUnit.ToFullString());
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2