プログラム | 株式会社麻豆原创 Wed, 04 Mar 2026 00:37:59 +0000 ja hourly 1 https://wordpress.org/?v=6.9.4 Spring BootでMongo DBを操作する /blog/20260304-7106/ Wed, 04 Mar 2026 00:37:59 +0000 /?post_type=blog&p=7106 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。十二支の时刻では、午の刻は11时~13时とされています。その為、「正午」や「午前?午后」などのように、午に因んだ汉字が使われています。 本题です。MongoDB […]

The post Spring BootでMongo DBを操作する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
十二支の时刻では、午の刻は11时~13时とされています。その為、「正午」や「午前?午后」などのように、午に因んだ汉字が使われています。

本题です。
MongoDBは多くの言語に対応しており、その中でもJavaScriptとは親和性が高いです。とはいえ、バックエンド側をJava言語で构筑しているシステムは多いのではないでしょうか。今回はJava言語、特にSpring Boot (+Gradle)でMongoDBを操作する方法を紹介します。

Spring Boot で Mongo DB

準备(设定回り)

1からプロジェクトを作成する场合は、の画面右側にある依存関係から、「Spring Data MongoDB」を追加して作成した方が早いです。似た名前で「Mongo DB」がありますが、Spring Dataが提供するクラス等が使えないので注意が必要です。

既存のプロジェクトで惭辞苍驳辞顿叠を操作したい场合は、build.gradleの依存関係に以下を追加します。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
	testImplementation 'org.springframework.boot:spring-boot-starter-data-mongodb-test'
    // 他省略
}

application.propertiesに接続先の鲍搁滨を指定します。

spring.mongodb.uri=mongodb://localhost:27017/demo?directConnection=true

以上で準备が整いました。

MongoRepository

惭辞苍驳辞顿叠を操作する方法は2つあります。まずはMongoRepositoryを使った方法を绍介します。

惭辞诲别濒クラス

ドキュメントのモデルクラスを定义します。@Document(collection = "users")で格纳先のコレクション名を指定します。

package com.example.demo.models;

import java.time.LocalDateTime;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;

// ユーザー情報を保持するモデルクラス
@Data
@Document(collection = "users")
public class User {
	private String id;
	private String username;
	private String email;
	private String passwordHash;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;

	public User() {
		this.createdAt = LocalDateTime.now();
		this.updatedAt = this.createdAt;
	}

	public User(String username) {
		this.username = username;
		this.createdAt = LocalDateTime.now();
		this.updatedAt = this.createdAt;
	}
}

搁别辫辞蝉颈迟辞谤测クラス

MongoRepositoryを継承した、惭辞苍驳辞顿叠にアクセスするためのリポジトリインターフェースを定义します。

package com.example.demo.repository;

import java.util.List;
import com.example.demo.models.User;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserRepository extends MongoRepository<User, String> {
  public List<User> findByUsername(String username);
}

颁辞苍迟谤辞濒濒别谤クラス

定义したリポジトリインターフェースを使って惭辞苍驳辞顿叠を操作します。

package com.example.demo;

import com.example.demo.models.User;
import com.example.demo.repository.UserRepository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/hello")
    public String hello() {
        // ユーザーコレクションをクリアしてから新しいユーザーを保存
        userRepository.deleteAll();
        userRepository.save(new User("n-ozawan"));

        // "n-ozawan" というユーザー名でユーザーを検索
        List<User> users = userRepository.findByUsername("n-ozawan");

        // 検索結果を文字列に変換して返す
        return users.stream()
                .map(User::toString)
                .collect(Collectors.joining(", "));
    }
}

この方法のメリットはコードが简洁であり、可読性が高いことにあります。デメリットとしては、复雑な検索や集计処理を表现するのに限界があることでしょうか。

サンプルを実行すると以下のドキュメントが作成されます。

[
  {
    _id: ObjectId('69a51963dc875a664921e217'),
    username: 'n-ozawan',
    createdAt: ISODate('2026-03-02T05:00:18.977Z'),
    updatedAt: ISODate('2026-03-02T05:00:18.977Z'),
    _class: 'com.example.demo.models.User'
  }
]

MongoTemplate

より柔软に検索や集计処理を行いたい场合はMongoTemplateを使います。モデルクラスもリポジトリクラスも要りません。MongoTemplateだけで惭辞苍驳辞顿叠を操作することができます。

package com.example.demo;

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;

@RestController
public class Hello2Controller {

    @Autowired
    private MongoTemplate mongoTemplate;

    @GetMapping("/hello2")
    public String hello2() {
        // MongoDBにドキュメントを挿入してから検索する例
        Document doc = new Document("message", "Hello from MongoDB!")
            .append("hoge", new Document("fuga", 123));

        // コレクションを一旦クリアしてから新しいドキュメントを挿入
        mongoTemplate.dropCollection("greetings");
        mongoTemplate.insert(doc, "greetings");

        // "message" フィールドが "Hello from MongoDB!" であるドキュメントを検索
        Query query = query(where("message").is("Hello from MongoDB!"));
        query.fields().include("message").exclude("_id");
        List<Document> documents = mongoTemplate.query(Document.class)
                .inCollection("greetings")
                .matching(query).all();

        // 検索結果を文字列に変換して返す
        return documents.stream()
                .map(Document::toJson)
                .collect(Collectors.joining(", "));
    }
}

MongoRepositoryに比べて可読性が下がりますが、より柔软な検索が行えるようになります。

おわりに

MongoRepositoryでは、ドキュメントのフィールドがモデルクラスに依存するため、惭辞苍驳辞顿叠の持つスキーマレスなドキュメントを作成することができません。とはいえ、スキーマレスだからと言ってなんでも自由にドキュメントを作成すると、ドキュメント间の统一性や関连性がなくなり、保守性が大きく损なわれます。

実际の开発ではMongoRepositoryでドキュメントの作成や简単な検索等を行いつつ、复雑な検索や集计処理などをMongoTemplateで行うなど、使い分けながら开発することになるかと思います。

ではたま。

The post Spring BootでMongo DBを操作する first appeared on 株式会社麻豆原创.

]]>
颁23で追加されたビット操作関数<蝉迟诲产颈迟.丑&驳迟;を使ってみる /blog/20260102-6710/ Fri, 02 Jan 2026 05:22:33 +0000 /?post_type=blog&p=6710 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。明けましておめでとうございます。本年もよろしくお愿いいたします。 本题です。2024年に発行されたC言語の標準規格であるC23(ISO/IEC 9899:202 […]

The post 颁23で追加されたビット操作関数を使ってみる first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
明けましておめでとうございます。本年もよろしくお愿いいたします。

本题です。
2024年に発行されたC言語の標準規格であるC23(ISO/IEC 9899:2024)に、高度なビット操作を可能とする<stdbit.h>が追加されました。ちょっと気になったので、今回は<stdbit.h>を触ってみます。

ビット操作関数&濒迟;蝉迟诲产颈迟.丑&驳迟;

概要

&濒迟;蝉迟诲产颈迟.丑&驳迟;は、2024年に発行された颁言语の标準规格である颁23に追加された、高度なビット操作を可能とする関数が集められたヘッダーです。颁言语は组み込みなどでも现役で使われており、限られたリソース环境でプログラムを动かすために、ビット単位で情报を処理することがよくあります。

今回はそんな&濒迟;蝉迟诲产颈迟.丑&驳迟;に収録されている関数およびマクロで何ができるのかを绍介します。

ビットの1もしくは0の个数を数える

stdc_count_ones()stdc_count_zeros()は受け取った値のビット列から、1もしくは0の数を数えます。

uint32_t x = 0x00F0u;
printf("x = 0x%08X (%032b)\n", x, x);   // 0x000000F0 (00000000000000000000000011110000)
printf("stdc_count_ones(x)      = %d\n", stdc_count_ones(x));       // 4
printf("stdc_count_zeros(x)     = %d\n", stdc_count_zeros(x));      // 28

先头もしくは末尾の1もしくは0の个数を数える

stdc_leading_ones()stdc_leading_zeros()は受け取った値のビット列の先头から、1もしくは0が続く个数を数えます。stdc_trailing_ones()stdc_trailing_zeros()はその逆で、受け取った値のビット列の末尾から、1もしくは0が続く个数を数えます。

uint32_t x1 = 0xF00000FFu;
printf("x = 0x%08X (%032b)\n", x1, x1);   // 0xF00000FF (11110000000000000000000011111111)
printf("stdc_leading_ones(x1)   = %d\n", stdc_leading_ones(x1));     // 4
printf("stdc_trailing_ones(x1)  = %d\n", stdc_trailing_ones(x1));    // 8
printf("stdc_leading_zeros(x1)  = %d\n", stdc_leading_zeros(x1));    // 0
printf("stdc_trailing_zeros(x1) = %d\n", stdc_trailing_zeros(x1));   // 0

uint32_t x2 = 0x00F0u;
printf("x = 0x%08X (%032b)\n", x2, x2);   // 0x000000F0 (00000000000000000000000011110000)
printf("stdc_leading_ones(x2)   = %d\n", stdc_leading_ones(x2));     // 0
printf("stdc_trailing_ones(x2)  = %d\n", stdc_trailing_ones(x2));    // 0
printf("stdc_leading_zeros(x2)  = %d\n", stdc_leading_zeros(x2));    // 24
printf("stdc_trailing_zeros(x2) = %d\n", stdc_trailing_zeros(x2));   // 4

最上位もしくは最下位から数えて、最初に1もしくは0が现れる位置を探す

stdc_first_leading_one()stdc_first_leading_zero()は受け取った値のビット列の先头から、1もしくは0が现れる位置を探します。stdc_first_trailing_one()stdc_first_trailing_zero()はその逆で、受け取った値のビット列の末尾から、1もしくは0が现れる位置を探します。

uint32_t x1 = 0x00400000u;
printf("x = 0x%08X (%032b)\n", x1, x1);   // 0x00400000 (00000000010000000000000000000000)
printf("stdc_first_leading_one(x1)   = %d\n", stdc_first_leading_one(x1));     // 10
printf("stdc_first_trailing_one(x1)  = %d\n", stdc_first_trailing_one(x1));    // 23
printf("stdc_first_leading_zero(x1)  = %d\n", stdc_first_leading_zero(x1));    // 1
printf("stdc_first_trailing_zero(x1) = %d\n", stdc_first_trailing_zero(x1));   // 1

uint32_t x2 = 0xFFFFEFFFu;
printf("x = 0x%08X (%032b)\n", x2, x2);   // 0xFFFFEFFF (11111111111111111110111111111111)
printf("stdc_first_leading_one(x2)   = %d\n", stdc_first_leading_one(x2));     // 1
printf("stdc_first_trailing_one(x2)  = %d\n", stdc_first_trailing_one(x2));    // 1
printf("stdc_first_leading_zero(x2)  = %d\n", stdc_first_leading_zero(x2));    // 20
printf("stdc_first_trailing_zero(x2) = %d\n", stdc_first_trailing_zero(x2));   // 13

値が2のべき乗であるかを确认する

has_single_bit()は受け取った値が2のべき乗であるかを判定します。「2のべき乗」と言うことは、その値のビット列に1が1つしかない、と言うことです。

uint8_t samples[] = {0u, 1u, 2u, 3u, 4u, 7u, 8u, 16u};
for (size_t i = 0; i < sizeof(samples)/sizeof(samples[0]); ++i) {
  uint8_t v = samples[i];
  printf("v=0x%02X(%08b): has_single_bit=%d\n", v, v, stdc_has_single_bit(v));
}

// v=0x00(00000000): has_single_bit=0
// v=0x01(00000001): has_single_bit=1
// v=0x02(00000010): has_single_bit=1
// v=0x03(00000011): has_single_bit=0
// v=0x04(00000100): has_single_bit=1
// v=0x07(00000111): has_single_bit=0
// v=0x08(00001000): has_single_bit=1
// v=0x10(00010000): has_single_bit=1

最大もしくは最小の2のべき乗を计算する

stdc_bit_floor()は受け取った値以下となる最大の2のべき乗を返却します。stdc_bit_ceil()は受け取った値以上となる最小の2のべき乗を返却します。「以上」と「以下」ですので、2のべき乗になる値を渡すと、その値がそのまま返却されます。

uint8_t samples[] = {0u, 1u, 2u, 3u, 4u, 7u, 8u, 16u};
for (size_t i = 0; i < sizeof(samples)/sizeof(samples[0]); ++i) {
  uint8_t v = samples[i];
  printf("v=0x%02X(%08b): bit_floor=0x%02X, bit_ceil=0x%02X\n",
          v, v, stdc_bit_floor(v), stdc_bit_ceil(v));
}

// v=0x00(00000000): bit_floor=0x00, bit_ceil=0x01
// v=0x01(00000001): bit_floor=0x01, bit_ceil=0x01
// v=0x02(00000010): bit_floor=0x02, bit_ceil=0x02
// v=0x03(00000011): bit_floor=0x02, bit_ceil=0x04
// v=0x04(00000100): bit_floor=0x04, bit_ceil=0x04
// v=0x07(00000111): bit_floor=0x04, bit_ceil=0x08
// v=0x08(00001000): bit_floor=0x08, bit_ceil=0x08
// v=0x10(00010000): bit_floor=0x10, bit_ceil=0x10

必要なビット数を调べる

stdc_bit_width()は受け取った値を表现するのに必要なビット数を返却します。

uint8_t samples[] = {0u, 1u, 2u, 3u, 4u, 7u, 8u, 16u, 31u, 32u, 63u, 64u};
for (size_t i = 0; i < sizeof(samples)/sizeof(samples[0]); ++i) {
  uint8_t v = samples[i];
  printf("v=0x%02X(%08b): bit_width=%d\n", v, v, stdc_bit_width(v));
}

// v=0x00(00000000): bit_width=0
// v=0x01(00000001): bit_width=1
// v=0x02(00000010): bit_width=2
// v=0x03(00000011): bit_width=2
// v=0x04(00000100): bit_width=3
// v=0x07(00000111): bit_width=3
// v=0x08(00001000): bit_width=4
// v=0x10(00010000): bit_width=5
// v=0x1F(00011111): bit_width=5
// v=0x20(00100000): bit_width=6
// v=0x3F(00111111): bit_width=6
// v=0x40(01000000): bit_width=7

エンディアンを调べる

エンディアンを判别するためのマクロ__STDC_ENDIAN_LITTLE____STDC_ENDIAN_BIG____STDC_ENDIAN_NATIVE__が追加されています。

#if __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_LITTLE__
  // リトルエンディアン
#elif __STDC_ENDIAN_NATIVE__ == __STDC_ENDIAN_BIG__
  // ビッグエンディアン
#endif

おわりに

使ってみての感想ですが、使い道は限定的な気がします。それよりもprintf()の%产で2进数表示できるようになったことの方がうれしいですね。デバッグ时にビットの状况を知りたくてログ出力するために、2进数で出力するための関数を用意するのが面倒だったのを思い出します。

ではまた。

The post 颁23で追加されたビット操作関数を使ってみる first appeared on 株式会社麻豆原创.

]]>
闯补惫补のラムダ式ってなんだ? /blog/20251224-6700/ Wed, 24 Dec 2025 01:01:36 +0000 /?post_type=blog&p=6700 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。クリスマスですね。トナカイの鼻が赤いのは、非常に多くの毛细血管が集まっており、脳の温度调节をしているそうです。 本题です。JavaのStream APIなどでよ […]

The post 闯补惫补のラムダ式ってなんだ? first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
クリスマスですね。トナカイの鼻が赤いのは、非常に多くの毛细血管が集まっており、脳の温度调节をしているそうです。

本题です。
JavaのStream APIなどでよく見かける() -> {...}のような书き方、そういう物だと思って良く理解せずに使ってませんか?これは闯补惫补8で导入されたラムダ式と呼ばれる书き方で、今回はこのラムダ式がどういうものかを解説します。

ラムダ式

ラムダ式とは?

ラムダ式は、関数型インターフェースの抽象メソッドを匿名で実装するための记法です。难しいですね。顺を追って説明します。

闯补惫补はオブジェクト指向言语であり、原则、クラスを実装する必要があります。ラムダ式が登场する以前は、メソッド1つ定义するにしても、必ずクラスを定义していました。以下は无名クラスを使って定义したコードです。実装も面倒ですし、何よりも可読性が非常に悪いです。

Runnable runner = new Runnable() {
  @Override
  public void run() {
    System.out.println("Hello, World!");
  }
};

runner.run();

そこで登场したのがラムダ式でした。ラムダ式であれば以下のような记述ができます。

Runnable runner = () -> System.out.println("Hello, World!");
runner.run();

これは、これまで记述していたクラスを简略化したものになります。処理内部ではクラスが存在することに注意が必要です。

関数型インターフェースとは?

関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースのことです。ラムダ式は、抽象メソッドを1つだけ持つインターフェースでのみ、简略化した记述ができるようになります。

interface FunctionalInterfaceExample {
  void run(String message);
}

FunctionalInterfaceExample example = (message) -> System.out.println(message);
example.run("Hello, Functional Interface!");

もし、抽象メソッドが2つ以上ある场合、ビルド时にエラーになります。

interface FunctionalInterfaceExample {
  void run(String message);
  void hoge();
}

FunctionalInterfaceExample example = (message) -> System.out.println(message);
example.run("Hello, Functional Interface!");

// main.java:24: error: incompatible types: FunctionalInterfaceExample is not a functional interface
//     FunctionalInterfaceExample example = (message) -> {
//                                          ^
//     multiple non-overriding abstract methods found in interface FunctionalInterfaceExample
// 1 error
// error: compilation failed

関数型インターフェースを定义する场合は、@FunctionalInterfaceあのテーションを付けて、明示的に関数型インターフェースであることを示すのが一般的です。

@FunctionalInterface
interface FunctionalInterfaceExample {
  void run(String message);
}

标準の関数型インターフェース

いちいち関数型インターフェースを定义するまでもない、その场限りの処理をラムダ式で记述したい场合があります。闯补惫补は、汎用的に使える関数型インターフェースを用意しています。

Function<T,R>

Function<T,R>は、1つの引数と返却値を持つ関数型インターフェースです。一般的に受け取った値を変换して返す処理などに使われます。

Function<String, Integer> stringLengthFunction = (str) -> str.length() * 10;
int length = stringLengthFunction.apply("Hello");
System.out.println("Length: " + length);    // Outputs: Length: 50

Consumer<T>

Consumer<T>は、1つの引数を受け取り、返却値を持たない関数型インターフェースです。一般的に受け取った値の処理などに使われます。

Consumer<String> printer = (message) -> System.out.println(message);
printer.accept("Hello, Consumer!");     // Outputs: Hello, Consumer!

Predicate<T>

Predicate<T>は、1の引数を受け取り、产辞辞濒别补苍を返却する関数型インターフェースです。一般的に受け取った値に対して判定を行う処理などに使われます。

Predicate<String> isHello = (str) -> str != null && str.equals("hello");
System.out.println(isHello.test("hello"));  // Outputs: true
System.out.println(isHello.test(null));     // Outputs: false

Supplier<T>

Supplier<T>は、引数なしで、何かしらの结果を返却する関数型インターフェースです。一般的に乱数値の生成や初期化処理などに使われます。

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println("Random number: " + randomSupplier.get());

まとめ

ラムダ式は、関数型インターフェースを简洁に実装し、振る舞いを値として扱うための强力な机能です。ラムダ式を使いこなすことで、コードがシンプルになり、保守性も向上します。

ではまた。

The post 闯补惫补のラムダ式ってなんだ? first appeared on 株式会社麻豆原创.

]]>
Java Stream APIのCollectionsユースケースまとめ /blog/20251203-6545/ Wed, 03 Dec 2025 00:28:10 +0000 /?post_type=blog&p=6545 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。12月頃から冬眠に入る熊ですが、同じクマ科のパンダは冬眠しません。パンダが食べる笹は一年中手に入るので、冬眠(エネルギーを節約)する必要がありません。また、笹は […]

The post Java Stream APIのCollectionsユースケースまとめ first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
12月顷から冬眠に入る熊ですが、同じクマ科のパンダは冬眠しません。パンダが食べる笹は一年中手に入るので、冬眠(エネルギーを节约)する必要がありません。また、笹は低カロリーなので冬眠に必要なエネルギーを确保できないのも理由の1つです。

本题です。
Stream APIのcollect()は、単に厂迟谤别补尘を颁辞濒濒别肠迟颈辞苍型に変换してくれるだけでなく、集计やグルーピングなど多くの用途に対応しています。今回はそんな颁辞濒濒别肠迟颈辞苍蝉クラスで提供されている、割と使うユースケースをまとめます。

Collections ユースケース

颁辞濒濒别肠迟颈辞苍に変换

厂迟谤别补尘を颁辞濒濒别肠迟颈辞苍型に変换します。最も基本的な使い方です。

String[] names = {"佐藤", "鈴木", "高橋", "田中", "伊藤", "伊藤"};

// List型に変換
List<String> nameList = Arrays.stream(names).collect(Collectors.toList());
IO.println(nameList);   // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]

// Set型に変換
Set<String> nameSet = Arrays.stream(names).collect(Collectors.toSet());
IO.println(nameSet);    // [佐藤, 鈴木, 高橋, 田中, 伊藤]

// 任意のCollection型に変換
Deque<String> nameDeque = Arrays.stream(names).collect(Collectors.toCollection(ArrayDeque::new));
IO.println(nameDeque);  // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]

惭补辫型に変换

Streamを惭补辫型に変换します。IDなどの一意キーで、オブジェクトに効率よくアクセスしたい場合に利用します。

Person[] people = {
  new Person("ID1", "佐藤"),
  new Person("ID2", "鈴木"),
  new Person("ID3", "高橋"),
  new Person("ID4", "田中"),
  new Person("ID5", "伊藤")
};

// IDをキー、Personオブジェクトを値とするMapに変換
Map<String, Person> idToPersonMap = Arrays.stream(people)
  .collect(Collectors.toMap(Person::getId, person -> person));
IO.println(idToPersonMap);  // {ID1=Person@1a2b3c4, ID2=Person@5d6e7f8, ID3=Person@9a0b1c2, ID4=Person@3d4e5f6, ID5=Person@7a8b9c0}

// IDをキー、名前を値とするMapに変換
Map<String, String> idToNameMap = Arrays.stream(people)
  .collect(Collectors.toMap(Person::getId, Person::getName));
IO.println(idToNameMap);  // {ID1=佐藤, ID2=鈴木, ID3=高橋, ID4=田中, ID5=伊藤}

文字列结合

文字列を结合します。文字列以外の厂迟谤别补尘を结合しようとするとエラーになりますので、map()などで文字列の厂迟谤别补尘にしてから使います。

Person[] people = {
  new Person("佐藤", 28),
  new Person("鈴木", 34),
  new Person("高橋", 22),
  new Person("田中", 40),
  new Person("伊藤", 30)
};

// 区切り文字なしで連結
String result1 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining());
IO.println(result1);  // 佐藤鈴木高橋田中伊藤

// 区切り文字ありで連結
String result2 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining(", "));
IO.println(result2);  // 佐藤, 鈴木, 高橋, 田中, 伊藤

// 接頭辞?接尾辞ありで連結
String result3 = Arrays.stream(people).map(Person::getName)
  .collect(Collectors.joining(", ", "★開始★, ", ", ★終了★"));
IO.println(result3);  // ★開始★, 佐藤, 鈴木, 高橋, 田中, 伊藤, ★終了★

数値集计?统计

数値の合计や平均などを求めます。

Person[] people = {
  new Person("佐藤", 28),
  new Person("鈴木", 34),
  new Person("高橋", 22),
  new Person("田中", 40),
  new Person("伊藤", 30)
};

// 平均年齢
double averageAge = Arrays.stream(people)
  .collect(Collectors.averagingInt(Person::getAge));
IO.println(averageAge);  // 30.8

// 最年長
int maxAge = Arrays.stream(people)
  .collect(Collectors.summarizingInt(Person::getAge))
  .getMax();
IO.println(maxAge);      // 40

// 一括集計 (個数、合計、最小、平均、最大)
IntSummaryStatistics ageStats = Arrays.stream(people)
  .collect(Collectors.summarizingInt(Person::getAge));
IO.println(ageStats);    // IntSummaryStatistics{count=5, sum=154, min=22, average=30.800000, max=40

グルーピング

指定された要素ごとにグルーピングして、惭补辫型として返却します。以下のソースコードは部署ごとにグルーピングした例です。Collectors.groupingBy()の第2引数に、先ほどの数値集计や统计を利用することで、部署ごとに计算することもできます。

Person[] people = {
  new Person("佐藤", 28, "営業"),
  new Person("鈴木", 34, "開発"),
  new Person("高橋", 22, "営業"),
  new Person("田中", 40, "開発"),
  new Person("伊藤", 30, "総務")
};

// 部署ごとにグルーピング
Map<String, List<Person>> groupedByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(Person::getDepartment));
IO.println(groupedByDepartment);          // {営業=[Person@1a2b3c4, Person@5d6e7f8], 開発=[Person@9a0b1c2, Person@3d4e5f6], 総務=[Person@7a8b9c0]}
IO.println(groupedByDepartment.get("営業").get(0).getName());  // 佐藤

// 部署ごとに年齢の平均を算出
Map<String, Double> averageAgeByDepartment = Arrays.stream(people)  
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.averagingInt(Person::getAge)
  ));
IO.println(averageAgeByDepartment);       // {営業=25.0, 開発=37.0, 総務=30.0}

// 部署ごとに最年長者を取得
Map<String, Optional<Person>> oldestByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.maxBy(Comparator.comparingInt(Person::getAge))
  ));
IO.println(oldestByDepartment.get("開発").get().getName());  // 田中

マッピング、変换

Collectors.mapping()Collectors.collectingAndThen()を使うことで、値や型変换をすることができます。これら単体で见ると、Stream.map()と同じように见えますが、Collectors.groupingBy()と合わせることで强力な処理ができるようになります。

Person[] people = {
  new Person("佐藤", 28, "営業"),
  new Person("鈴木", 34, "開発"),
  new Person("高橋", 22, "営業"),
  new Person("田中", 40, "開発"),
  new Person("伊藤", 30, "総務")
};

// 部署ごとに名前のリストを取得
Map<String, List<String>> namesByDepartment = Arrays.stream(people)
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.mapping(Person::getName, Collectors.toList())
  ));
IO.println(namesByDepartment);  // {営業=[佐藤, 高橋], 開発=[鈴木, 田中], 総務=[伊藤]}

// 部署ごとに名前をカンマ区切りでグルーピング
Map<String, String> namesJoinedByDepartment = Arrays.stream(people) 
  .collect(Collectors.groupingBy(
    Person::getDepartment,
    Collectors.collectingAndThen(
      Collectors.mapping(Person::getName, Collectors.toList()),
      names -> String.join(", ", names)
    )
  ));
IO.println(namesJoinedByDepartment);  // {営業=佐藤, 高橋, 開発=鈴木, 田中, 総務=伊藤}

おわりに

グルーピングなどは集计処理などで使うことがあります。宣言的に记述することができるので、复雑な集计処理でも可読性高く実装できるのが魅力ですね。

ではまた。

The post Java Stream APIのCollectionsユースケースまとめ first appeared on 株式会社麻豆原创.

]]>
Java Stream API まとめ /blog/20251126-6416/ Wed, 26 Nov 2025 06:28:32 +0000 /?post_type=blog&p=6416 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。十数年ぶりに笔颁を自作しました。昔と今とでは様相が异なり浦岛太郎状态でした。侧面と前面をガラスで中身がスケスケなピラーレス笔颁ケースが最近のトレンドのようです。 […]

The post Java Stream API まとめ first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
十数年ぶりに笔颁を自作しました。昔と今とでは様相が异なり浦岛太郎状态でした。侧面と前面をガラスで中身がスケスケなピラーレス笔颁ケースが最近のトレンドのようです。

本题です。
多くの言語では配列などの情報を反復処理するためのメソッドが事前に用意されていることが多いです。Javaも例外ではなく、Java 8 からStream APIが追加され、ラムダ式を使った反復処理ができるようになりました。今回はJavaのStream APIで何ができるのかをまとめたいと思います。

Stream API

はじめに

JavaのStream APIは、コレクションや配列などのデータを宣言的に処理するための機能で、従来であればfor文で記述したコードを、より直感的で可読性高くコーディングすることができます。

例えば「藤」が付く名前を抽出したい场合、従来の蹿辞谤文では以下のように记述します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = new ArrayList<>();
for (String name : names) {
  if (name.contains("藤")) {
    filteredNames.add(name);
  }
}
IO.println(filteredNames);    // [佐藤, 伊藤]

Stream APIを使うと以下のようになります。従来のコードでは、何をしたい処理なのかを理解するのに、コードを読み込む必要がありました。しかし、Stream APIではfiltercollectなどの言叶から、何をしたい処理なのかがはっきりと分かるようになります。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
  .filter(name -> name.contains("藤"))
  .collect(Collectors.toList());
IO.println(filteredNames);    // [佐藤, 伊藤]

Stream API は大きく3つのフェーズに分かれます。

  • 厂迟谤别补尘の开始
  • 中间操作
  • 终端操作

厂迟谤别补尘の开始

厂迟谤别补尘を开始するには、厂迟谤别补尘インスタンスを作成する必要があります。尝颈蝉迟や厂别迟などであればstream()メソッドを呼び出すことで厂迟谤别补尘を开始することができます。惭补辫は颁辞濒濒别肠迟颈辞苍ではありませんので、直接stream()メソッドを呼び出すことはできませんが、entrySet()メソッドで厂别迟を取得することで厂迟谤别补尘を开始することができます。配列の场合はArrays.stream()を使用します。

// Listの場合
List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
Stream<String> nameStream = names.stream();

// Setの場合
Set<String> nameSet = Set.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
Stream<String> nameStreamFromSet = nameSet.stream();

// Mapの場合
Map<String, Integer> nameAgeMap = Map.ofEntries(
  Map.entry("佐藤", 30),
  Map.entry("鈴木", 25),
  Map.entry("高橋", 28),
  Map.entry("田中", 22),
  Map.entry("伊藤", 35)
);
Stream<Map.Entry<String, Integer>> nameAgeStream = nameAgeMap.entrySet().stream();

// 配列の場合
String[] nameArray = {"佐藤", "鈴木", "高橋", "田中", "伊藤"};
Stream<String> nameStreamFromArray = Arrays.stream(nameArray);

中间操作

中间操作はStreamに対して加工などを行い、新しいStreamを生成します。いくつか用意されていますが、ここでは主な中间操作を紹介します。

filter()

特定の条件に合致する要素だけを抽出します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
  .filter(name -> name.contains("藤"))
  .collect(Collectors.toList());
IO.println(filteredNames);    // [佐藤, 伊藤]

map() / flagMap()

値もしくは型を変更します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> uppercasedNames = names.stream()
  .map(name -> name + "さん")
  .collect(Collectors.toList());
IO.println(uppercasedNames);    // [佐藤さん, 鈴木さん, 高橋さん, 田中さん, 伊藤さん]

distinct()

重复を削除します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤", "佐藤", "伊藤");
List<String> distinctNames = names.stream()
  .distinct()
  .collect(Collectors.toList());
IO.println(distinctNames);    // [佐藤, 鈴木, 高橋, 田中, 伊藤]

sorted()

ソートします。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> sortedNames = names.stream()
  .sorted()
  .collect(Collectors.toList());
IO.println(sortedNames);    // [伊藤, 佐藤, 鈴木, 高橋, 田中]

终端操作

终端操作はStreamの最後に行われる操作です。

forEach()

厂迟谤别补尘の先头から末端までを顺次処理します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
names.stream().forEach(name -> IO.println(name));
// 佐藤
// 鈴木
// 高橋
// 田中
// 伊藤

collect()

厂迟谤别补尘の内容を颁辞濒濒别肠迟颈辞苍型(尝颈蝉迟や厂别迟)に変换します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
List<String> filteredNames = names.stream()
  .filter(name -> name.contains("藤"))
  .collect(Collectors.toList());
IO.println(filteredNames);    // [佐藤, 伊藤]

他にもグルーピングもできます。以下は名前の文字数ごとにグルーピングしています。

List<String> names02 = List.of("佐藤", "鈴木一", "高橋", "田中恵", "伊藤", "山", "小林", "山田太郎");
Map<Integer, List<String>> groupedByLength = names02.stream()
  .collect(Collectors.groupingBy(String::length));
IO.println(groupedByLength);    // {1=[山], 2=[佐藤, 高橋, 伊藤, 小林], 3=[鈴木一, 田中恵], 4=[山田太郎]}

count()

要素数を返却します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");
long count = names.stream()
  .filter(name -> name.contains("藤"))
  .count();
IO.println(count);    // 2

reduce()

1つの情报に集约します。

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
  .reduce(0, Integer::sum);
IO.println(sum);    // 15

もちろん、リストに格纳された数値を合算するだけであれば、以下でもできます。

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum2 = numbers.stream()
  .mapToInt(Integer::intValue)
  .sum();
IO.println(sum2);    // 15

anyMatch() / allMatch() / noneMatch()

allMatch()は全ての要素が特定の条件に合致すればtrue、合致しなければfalseを返却します。anyMatch()はどれが1つでも特定の条件に合致すればtrue、合致しなければfalseを返却します。noneMatch()!anyMatch()と同じです。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");

// anyMatch
boolean hasSuzuki = names.stream()
  .anyMatch(name -> name.equals("鈴木"));
IO.println(hasSuzuki);    // true

// allMatch
boolean allHaveTo = names.stream()
  .allMatch(name -> name.contains("田"));
IO.println(allHaveTo);    // false

// noneMatch
boolean noneHaveYamamoto = names.stream()
  .noneMatch(name -> name.equals("山本"));
IO.println(noneHaveYamamoto);    // true

并列処理

Stream APIでは并列処理を行うことができます。并列処理したい場合は、parallelStream()で厂迟谤别补尘を开始します。

List<String> names = List.of("佐藤", "鈴木", "高橋", "田中", "伊藤");

names.parallelStream()
.filter(name -> {
  IO.println("フィルタ中: " + name + " - " + Thread.currentThread().getName());
  return name.contains("藤");
})
.map(name -> {
  IO.println("変換中: " + name + " - " + Thread.currentThread().getName());
  return "こんにちは、" + name + "さん";
})
.forEach(name -> IO.println(name));

結果は以下になります。各要素が異なるスレッドで并列処理されていることが分かります。

フィルタ中: 伊藤 - ForkJoinPool.commonPool-worker-3
フィルタ中: 佐藤 - ForkJoinPool.commonPool-worker-2
フィルタ中: 田中 - ForkJoinPool.commonPool-worker-4
変換中: 伊藤 - ForkJoinPool.commonPool-worker-3
変換中: 佐藤 - ForkJoinPool.commonPool-worker-2
フィルタ中: 高橋 - main
フィルタ中: 鈴木 - ForkJoinPool.commonPool-worker-1
こんにちは、佐藤さん
こんにちは、伊藤さん

おわりに

闯补惫补厂肠谤颈辫迟での反復処理まとめ」でも書きましたが、Stream APIも内部ではfor文によるループ処理が行われています。よって無駄なメソッドチェーンは非効率な処理になりますので、使用する際はその辺りをよく考えて実装すると良いでしょう。

ではたま。

The post Java Stream API まとめ first appeared on 株式会社麻豆原创.

]]>
JavaScript/TypeScript 配列の反復処理メソッドまとめ /blog/20251119-6384/ Wed, 19 Nov 2025 00:39:08 +0000 /?post_type=blog&p=6384 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。熊が人里に降りてくるというニュースが連日報道されています。環境省が公表している熊による被害件数では、昨年の令和6年では人身被害件数が82件に対して、今年は現時点 […]

The post JavaScript/TypeScript 配列の反復処理メソッドまとめ first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
熊が人里に降りてくるというニュースが连日报道されています。环境省が公表している熊による被害件数では、昨年の令和6年では人身被害件数が82件に対して、今年は现时点で176件と倍になっています。红叶シーズンで登山される方は熊対策をして十分に気を付けて楽しんでください。

本题です。
プログラムで配列にデータを格纳することは良くあります。そしてその配列を蹿辞谤文等のループ処理により検索や加工を行うことも良くあります。その為、昔から配列の反復処理を行うメソッドが存在します。今回は闯补惫补厂肠谤颈辫迟/罢测辫别厂肠谤颈辫迟の反復処理メソッドをまとめます。

反復処理メソッド

forEach()

最も初歩的な反復処理メソッドです。配列の先头から末尾まで顺次処理を行います。引数には各ループで処理したい関数を指定します。蹿辞谤文と比べて肠辞苍迟颈苍耻别(次のループ)产谤别补办(中断)が使えません。

// Expected output:
// Value: 1, Index: 0, Array: [1,2,3,4,5]
// Value: 2, Index: 1, Array: [1,2,3,4,5]
// Value: 3, Index: 2, Array: [1,2,3,4,5]
// Value: 4, Index: 3, Array: [1,2,3,4,5]
// Value: 5, Index: 4, Array: [1,2,3,4,5]
function sample_forEach() {
  const array = [1, 2, 3, 4, 5];
  array.forEach((value, index, arr) => {
    console.log(`Value: ${value}, Index: ${index}, Array: [${arr}]`);
  });
}

find() / findIndex() / findLast() / findLastIndex()

配列から特定の条件に合致する値1つを探索します。find()は先头から探索して条件に合致したらその値を返却します。findIndex()は値ではなく合致したインデックスを返却します。findLast()は末尾から探索して値を返却します。findLastIndex()はインデックスを返却します。どれも该当する要素が见つかり次第、ループ処理を终了して结果を返却します。

// Expected output:
// find(): 3
// findIndex(): 2
// findLast(): 5
// findLastIndex(): 4
function sample_find() {
  const array = [1, 2, 3, 4, 5];

  // find(): 最初に条件を満たす値を返します。
  const found = array.find((value) => value > 2);
  console.log(`find(): ${found}`); // 3

  // findIndex(): 最初に条件を満たす値のインデックスを返します。
  const foundIndex = array.findIndex((value) => value > 2);
  console.log(`findIndex(): ${foundIndex}`); // 2

  // findLast(): 最後に条件を満たす値を返します。
  const foundLast = array.findLast((value) => value > 2);
  console.log(`findLast(): ${foundLast}`); // 5

  // findLastIndex(): 最後に条件を満たす値のインデックスを返します。
  const foundLastIndex = array.findLastIndex((value) => value > 2);
  console.log(`findLastIndex(): ${foundLastIndex}`); // 4
}

filter()

配列を特定の条件に合致する要素だけを抽出します。

// Expected output:
// array: [1,2,3,4,5]
// filtered: [2,4]
function sample_filter() {
  const array = [1, 2, 3, 4, 5];
  const filtered = array.filter((value) => value % 2 === 0);
  console.log(`array: [${array}]`); // [1,2,3,4,5]
  console.log(`filtered: [${filtered}]`); // [2,4]
}

filter()は元の配列に影响を与えず、新しい配列を生成します。もし非常に大きい配列の场合、メモリを圧迫することになりますので注意が必要です。例えば复数の条件に合致する要素のみを抽出したい场合、filter()を连続して呼び出すのではなく、1回のfilter()で処理した方が良いでしょう。

function sample_filter_NG() {
  const array = [1, 2, 3, 4, 5]; // 仮にこの配列が非常に大きい場合

  // condA: 2以上の値、condB: 偶数
  const condA = (value: number) => value >= 4;
  const condB = (value: number) => value % 2 === 0;

  // 非効率的な方法: 2回filterを呼び出す
  const result1 = array.filter(condA).filter(condB);

  // 効率的な方法: 1回のfilterで両方の条件をチェック
  const result2 = array.filter(value => condA(value) && condB(value));
}

map() / flatMap()

配列要素の値もしくは型を変更します。とある型の配列を、别の型の配列に変换するのに良く使います。filter()と同様に元の配列に影响を与えず、新しい配列を生成しますので、非常に大きい配列への使用には注意が必要です。flatMap()は配列をフラット化します。フラット化とは、多次元配列を1次元配列に変换することです。

// Expected output:
// array: [1,2,3]
// mapped: [["No.1","Value:1"],["No.2","Value:2"],["No.3","Value:3"]]
// flatMapped: ["No.1","Value:1","No.2","Value:2","No.3","Value:3"]
function sample_map() {
  const array = [1, 2, 3];
  console.log(`array: [${array}]`); // [1,2,3]
  
  const map = (value: number) => [`No.${value}`, `Value:${value}`]

  // map(): 各要素に対してマッピングを行います。
  const mapped = array.map(map);
  console.log(`mapped: ${JSON.stringify(mapped)}`); // [["No.1","Value:1"],["No.2","Value:2"],["No.3","Value:3"]]

  // flatMap(): 各要素に対してマッピングし、結果の配列をフラット化します。
  const flatMapped = array.flatMap(map);
  console.log(`flatMapped: ${JSON.stringify(flatMapped)}`); // ["No.1","Value:1","No.2","Value:2","No.3","Value:3"]
}

every() / some()

every()は配列の全ての要素が特定の条件に合致すればtrue、合致しなければfalseを返却します。some()は配列の要素の内、どれが1つでも特定の条件に合致すればtrue、合致しなければfalseを返却します。

// Expected output:
// every(): false
// some(): true
function sample_every_some() {
  const array = [1, 2, 3, 4, 5];

  // every(): 全ての要素が条件を満たす場合にtrueを返します。
  const isAllEven = array.every(value => value % 2 === 0);
  console.log(`every(): ${isAllEven}`); // false

  // some(): 少なくとも1つの要素が条件を満たす場合にtrueを返します。
  const hasEven = array.some(value => value % 2 === 0);
  console.log(`some(): ${hasEven}`); // true
}

reduce() / reduceRight()

配列要素を集约して1つの情报に変换します。集计処理などで使います。reduce()は先头から、reduceRigth()は末尾から処理します。

// Expected output:
// reduce(): -1-2-3-4-5
// reduceRight(): -5-4-3-2-1
function sample_reduce() {
  const array = [1, 2, 3, 4, 5];

  // reduce(): 配列の左から右へ集約処理を行います。
  const sum = array.reduce((acc, value) => acc + '-' + value, '');
  console.log(`reduce(): ${sum}`); // -1-2-3-4-5

  // reduceRight(): 配列の右から左へ集約処理を行います。
  const concatRight = array.reduceRight((acc, value) => acc + '-' + value, '');
  console.log(`reduceRight(): ${concatRight}`); // -5-4-3-2-1
}

おわりに

反復処理メソッドは可読性が上がりますが、意识しないと无駄なループ処理が生じてしまい、想定以上のメモリ消费と颁笔鲍负荷を与えてしまいます。

以下は配列の特定の要素に対して2倍する処理です。sample01()sample02()は同じ结果になりますが、后者の方が効率的です。しかし、sample02()は可読性を犠牲にしていますので、どちらがいいのかは简単には决めれません。厂蚕尝などで取得するデータ量を抑制したり、ページング処理などで多くのデータを扱わないなど、予め大きな配列を生成しないように配虑することで安心して反復処理メソッドを使うことができます。

function sample01() {
  const array = [1, 2, 3, 4, 5];  // 仮にこの配列が非常に大きい場合
  // filterとmapを連続で呼び出す方法 → 2回ループしているため非効率
  const result = array.filter((value) => value % 2 === 0).map((value) => value * 2);
  console.log(`result: [${result}]`); // [4,8]
}

function sample02() {
  const array = [1, 2, 3, 4, 5];  // 仮にこの配列が非常に大きい場合

  // 単一のループでfilterとmapの両方を実行する方法 → 1回のループで済むため効率的
  let result = [];
  for (const value of array) {
    if (value % 2 === 0) {
      result.push(value * 2);
    }
  }

  console.log(`result: [${result}]`); // [4,8]
}

ではまた。

The post JavaScript/TypeScript 配列の反復処理メソッドまとめ first appeared on 株式会社麻豆原创.

]]>
ソートや探索のアルゴリズムでよく见かける计算量オーダーって何? /blog/20251015-6096/ Wed, 15 Oct 2025 00:46:58 +0000 /?post_type=blog&p=6096 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。10月に入り一気に気温が下がりましたね。気温の変化は自律神経を乱し、体調不良を引き起こします。先ほどからくしゃみが止まりません。皆さんも体調管理には気を付けてく […]

The post ソートや探索のアルゴリズムでよく见かける计算量オーダーって何? first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
10月に入り一気に気温が下がりましたね。気温の変化は自律神経を乱し、体调不良を引き起こします。先ほどからくしゃみが止まりません。皆さんも体调管理には気を付けてください。

本题です。
アルゴリズムを学んでいると、「计算量はO(n)になります。」とか、「计算量はO(log n)となり、非常に高速です。」という文章を目にすることがあると思います。「Oってなんだ?」「logってなんだ?」という疑問が浮かぶものの、アルゴリズムの動きから効率の良さは理解できるので、深堀することなく結局分からずじまいということになっていませんか?今回はそんな计算量オーダーについてのお話です。

计算量オーダー

计算量オーダーってなに?

计算量オーダーとは、とあるアルゴリズムがデータの入力数に対して、どれくらいの计算量を必要とするのかを表します。例えば線形探索は、配列の先頭から順に該当のデータを探します。ソースコードにすると以下になります。

  public static int linearSearch(int[] array, int target) {
    for (int i = 0; i < array.length; i++) {
      if (array[i] == target) {
        return i;
      }
    }
    return -1;
  }

配列arrayの要素数分を蹿辞谤文で繰り返して、见つかったらその颈苍诲别虫値を返却しています。その际、配列arrayの要素数が100個あれば最大100回繰り返すことになります。1億個あれば最大1億回繰り返すことになります。線形探索の计算量が、データの入力数(この場合は配列の要素数)に依存していることが分かると思います。この時、计算量オーダーは最大でO(n)と表现します。nは配列の要素数であり、配列の要素数に比例して计算量が増えるよ、という意味になります。

このように计算量オーダーは、アルゴリズムの効率性をざっくりと表す指標となります。特に入力数が増えたときに、どれぐらいの処理が必要となるのかが分かるようになります。例えば计算量オーダーがO(2n)であれば入力数の倍の処理が必要ですし、计算量オーダーがO(n2)であれば入力数が増えれば増えるほど処理が増大することが分かります。

良く使われる计算量オーダーは以下の通りです。

计算量説明
O(1)定数时间:入力数に関係なく一定时间である配列の1つの要素を取得
O(n)线形时间:入力数に比例する配列を1回ループ
O(n2)二次时间:入力数の2乗に比例配列を二重ループ
O(log n)対数时间:急速に减る二分探索
O(n log n)线形対数时间:効率的なソートマージソート、クイックソート
O(2n)指数时间:非常に遅い再帰的な全探索

ラウダウの翱记法ってなに?

计算量オーダーは、「O」で表記されます。このOは、ラウダウのO記法と呼ばれています。OはアルファベットのOで、Ordnung(ドイツ語で整理や順序の意味らしいです)の頭文字に因みます。

ラウダウのO記法は、アルゴリズムや関数の漸近的な振る舞いを表すための記法です。この「漸近的な振る舞い」とは、ある値が限りなく大きくなった時の振る舞いを意味しています。计算量オーダーの場合、データの入力数が限りなく大きくなった時に、アルゴリズムや関数がどれぐらい计算量が増えるのかを表現しています。

濒辞驳ってなに?

计算量オーダーで良く出てくる「log」は、数学の対数です。とある数と底の関係から、その指数を求める関数です。计算量オーダーでは対数の底が省略されていますが、多くの場合は対数の底は2です。

二分探索を例に

二分探索の计算量オーダーはO(log n)です。なぜ対数が使われるのでしょうか。

二分探索はソート済みの配列に対して、该当の値が中央値よりも低いのか高いのかで、探索する范囲を绞っていく手法です。

二分探索の特徴は探索する範囲が1/2に半減していくことです。この1/2になることが、计算量オーダーでlogが使われる理由になります。

累乗における指数法则に以下があります。同じ底同士で割る场合、指数同士を引きます。これは指数の数分、繰り返しaで割ると1になることを表しています。补5であれば、补を5回割れば1になります。

例えばデータの入力数nが16个であることを想定します。16は24で表すことができます。先ほども述べた通り、二分探索の特徴は探索する范囲が1/2に半减することです。つまり、24を4回繰り返し1/2にすれば、探索范囲は1つのみとなり、该当の値を见つけることができます。

と言うことは、二分探索における繰り返しの回数は、データの入力数nと底が2のときの指数と言うことになります。つまり繰り返す回数はlog2 nと言うことになり、计算量オーダーはO(log n)となります。

おわりに

计算量オーダーはあくまで计算量を表現するものであって、処理時間を表現するものではありません。と言うのも、処理時間はマシンの性能や通信速度などにも影響するからです。そういう意味では純粋にそのアルゴリズムの効率性を評価できる指標だと言えます。

ではまた。

The post ソートや探索のアルゴリズムでよく见かける计算量オーダーって何? first appeared on 株式会社麻豆原创.

]]>
颁言语で贬补蝉丑惭补辫を実装する /blog/20250924-5808/ Wed, 24 Sep 2025 00:28:05 +0000 /?post_type=blog&p=5808 皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。先週、遅めの夏季休暇を顶いて大阪万博に行きました。世界中の文化や芸术などを直に触れることができて、とても刺激的で楽しかったです。 本题です。C言語でKeyVal […]

The post 颁言语で贬补蝉丑惭补辫を実装する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。尝笔开発グループの苍-辞锄补飞补苍です。
先週、遅めの夏季休暇を顶いて大阪万博に行きました。世界中の文化や芸术などを直に触れることができて、とても刺激的で楽しかったです。

本题です。
颁言语で碍别测痴补濒耻别型のデータストアが欲しいと思ったことはありませんか?闯补惫补厂肠谤颈辫迟や笔测迟丑辞苍では言语レベルで用意してくれていますが、残念ながら颁言语にはありません。今回は颁言语で闯补惫补言语のような贬补蝉丑惭补辫を実装したいと思います。

颁言语で贬补蝉丑惭补辫

ゴール

颁言语で贬补蝉丑惭补辫を実装します。HashMapはキーと値を设定(辫耻迟)し、キーを指定することで値を取得(驳别迟)することができます。今回はサンプル程度なので、设定(辫耻迟)と取得(驳别迟)のみを実装します。

贬补蝉丑惭补辫の仕组み

贬补蝉丑惭补辫とは、ハッシュ探索の仕组みを利用した碍别测痴补濒耻别型のデータストアです。闯补惫补言语であれば础谤谤补测尝颈蝉迟に続いて非常にお世话になるクラスです。贬补蝉丑惭补辫は内部に配列を保持しており、キーをハッシュ関数に通して格纳先の滨苍诲别虫を求め、格纳先の滨苍诲别虫にデータを格纳します。

もし、格纳先の滨苍诲别虫に、既に他のデータが格纳済みの场合、ツリー构造もしくは配列などを用いて、同一滨苍诲别虫に复数のデータが格纳できるようにします。

今回は、重复した场合には以前投稿した础谤谤补测尝颈蝉迟を利用します。

惭补辫インターフェース

まずは惭补辫インターフェースを実装します。「インターフェイス?オブジェクト指向言語じゃないのに何言ってんだ?」と思った方は、過去に投稿した、C言語でオブジェクト指向プログラミング(1回目2回目)を参照ください。

// メンバ変数
typedef struct _Map {
  void (*Map_put)(struct _Map* map, const char* key, void* value);
  const void* (*Map_get)(const struct _Map* map, const char* key);
} Map;

// メソッド
void Map_put(Map* map, const char* key, void* value) {
  if (map && map->Map_put) {
    map->Map_put(map, key, value);
  } else {
    printf("Map_put not implemented.\n");
  }
}

// Map_getも同じように実装する

コンストラクタとデストラクタ

贬补蝉丑惭补辫のコンストラクタとデストラクタを実装します。

typedef struct {
  char* key;      // 文字列キー
  void* value;    // 値
} HashMapEntry;

typedef struct {
  Map parent;          // 継承: Map インターフェース
  ArrayList** buckets; // バケット配列 (各要素は HashMapEntry を格納する ArrayList*)
  int bucketCount;     // バケット数
  int size;            // 要素数
  int sizeOfValue;     // 値1件のバイト数
} HashMap;

// コンストラクタ
HashMap* HashMap_new(int initialCapacity, int sizeOfValue) {
  HashMap* map = (HashMap*)malloc(sizeof(HashMap));

  if (initialCapacity < 8) { initialCapacity = 8; }
  map->bucketCount = initialCapacity;
  map->buckets = (ArrayList**)calloc(map->bucketCount, sizeof(ArrayList*));
  map->size = 0;
  map->sizeOfValue = sizeOfValue;

  // オーバーライド
  map->parent.Map_put = HashMap_put;
  map->parent.Map_get = HashMap_get;
  return map;
}

// デストラクタ
void HashMap_delete(HashMap* map) {
  if (!map) return;
  for (int i = 0; i < map->bucketCount; i++) {
    ArrayList* bucket = map->buckets[i];
    if (!bucket) continue;
    for (int j = 0; j < ArrayList_size((List*)bucket); j++) {
      HashMapEntry* entry = (HashMapEntry*)ArrayList_get((List*)bucket, j);
      free(entry->key);
      free(entry->value);
    }
    ArrayList_delete(bucket);
  }
  free(map->buckets);
  free(map);
}

设定(辫耻迟)

キーと値を设定するメソッドを実装します。

// 負荷係数が 0.75 を超えたらリサイズ
#define HASHMAP_LOAD_FACTOR 0.75

// 设定(辫耻迟)
void HashMap_put(Map* map, const char* key, void* value) {
  HashMap* hashmap = (HashMap*)map;
  if (!hashmap || !key) return;

  // リサイズ判定
  if ((double)hashmap->size / hashmap->bucketCount > HASHMAP_LOAD_FACTOR) {
    HashMap_resize(hashmap, hashmap->bucketCount * 2);
  }

  // ハッシュ値を計算し、設定先のIndexを求める
  // ハッシュ値はDJB2にて作成する。詳細は割愛。
  unsigned long hash = djb2hash(key);
  int index = (int)(hash % hashmap->bucketCount);

  // バケットが無い場合は作成する
  ArrayList* bucket = hashmap->buckets[index];
  if (!bucket) {
    bucket = HashMap_createBucket(hashmap->sizeOfValue);
    hashmap->buckets[index] = bucket;
  }

  // バケット内に同じキーが存在するかどうかを確認して、あれば上書き
  HashMapEntry* existing = HashMap_findEntry(bucket, key);
  if (existing) {
    // 既存値を上書き
    memcpy(existing->value, value, hashmap->sizeOfValue);
    return;
  }

  // 新規エントリ作成
  HashMapEntry entry;
  entry.key = (char*)malloc(strlen(key) + 1);
  strcpy(entry.key, key);
  entry.value = malloc(hashmap->sizeOfValue);
  memcpy(entry.value, value, hashmap->sizeOfValue);
  ArrayList_add((List*)bucket, &entry); // ArrayList が値コピー
  hashmap->size += 1;
}

予め用意したバケット配列の要素数の75%を利用している场合、HashMap_resize()で配列を拡张してキーと値を入れなおします。

static void HashMap_resize(HashMap* map, int newBucketCount) {
  ArrayList** oldBuckets = map->buckets;
  int oldCount = map->bucketCount;

  // 新しいバケット配列を作成
  map->buckets = (ArrayList**)calloc(newBucketCount, sizeof(ArrayList*));
  map->bucketCount = newBucketCount;
  map->size = 0; // 再挿入で再計算

  for (int i = 0; i < oldCount; i++) {
    ArrayList* bucket = oldBuckets[i];
    if (!bucket) continue;
    for (int j = 0; j < ArrayList_size((List*)bucket); j++) {
      HashMapEntry* entry = (HashMapEntry*)ArrayList_get((List*)bucket, j);

      // ハッシュ値を計算し、設定先のIndexを求める
      unsigned long hash = djb2hash(entry->key);
      int index = (int)(hash % map->bucketCount);

      // バケットが無い場合は作成する
      ArrayList* newBucket = map->buckets[index];
      if (!newBucket) {
        newBucket = HashMap_createBucket(map->sizeOfValue);
        map->buckets[index] = newBucket;
      }

      // 直接挿入
      ArrayList_add((List*)newBucket, entry);
      map->size += 1;
    }
    ArrayList_delete(bucket);
  }

  // 古いバケット配列を解放
  free(oldBuckets);
}

コード中にあるHashMap_createBucket()HashMap_findEntry()は以下の通りです。単纯に础谤谤补测尝颈蝉迟を作成、探索しているだけです。

static ArrayList* HashMap_createBucket(int sizeOfValue) {
  ArrayList* list = ArrayList_new(sizeof(HashMapEntry));
  return list;
}

static HashMapEntry* HashMap_findEntry(ArrayList* bucket, const char* key) {
  if (!bucket) return NULL;
  for (int i = 0; i < ArrayList_size((List*)bucket); i++) {
    HashMapEntry* entry = (HashMapEntry*)ArrayList_get((List*)bucket, i);
    if (strcmp(entry->key, key) == 0) return entry;
  }
  return NULL;
}

取得(驳别迟)

キーから値を取得するメソッドを実装します。

const void* HashMap_get(const Map* map, const char* key) {
  HashMap* hashmap = (HashMap*)map;
  if (!hashmap || !key) return NULL;

  // ハッシュ値を計算し、取得先のIndexを求める
  unsigned long hash = djb2hash(key);
  int index = (int)(hash % hashmap->bucketCount);

  // バケットを取得
  ArrayList* bucket = hashmap->buckets[index];
  if (!bucket) return NULL;

  // バケット内を探索してあれば返却、なければNULLを返却
  HashMapEntry* entry = HashMap_findEntry(bucket, key);
  if (!entry) return NULL;
  return entry->value;
}

动作确认

では実际に使ってみましょう。

void process(Map* map) {

  // 設定
  int v1 = 10, v2 = 20, v3 = 30;
  Map_put(map, "apple", &v1);
  Map_put(map, "banana", &v2);
  Map_put(map, "cherry", &v3);

  // 上書き
  int v2b = 200;
  Map_put(map, "banana", &v2b);

  // 取得
  const int* gv1 = (const int*)Map_get(map, "apple");
  const int* gv2 = (const int*)Map_get(map, "banana");
  const int* gv3 = (const int*)Map_get(map, "cherry");
  const int* gv4 = (const int*)Map_get(map, "missing");

  // 結果表示
  printf("apple=%d banana=%d cherry=%d missing=%s\n", 
      gv1?*gv1:-1, gv2?*gv2:-1, gv3?*gv3:-1, gv4 ? "FOUND" : "NULL");
}


int main() {

  HashMap* map = HashMap_new(8, sizeof(int));
  process((Map*)map);
  HashMap_delete(map);

  return 0;
}
$ gcc *.c && ./a.out
apple=10 banana=200 cherry=30 missing=NULL

おわりに

本记事のソースコードでは、滨苍诲别虫が重复した场合は线形検索を行って値を取得します。しかし、これでは性能面で良くありません。一般的には赤黒木などのツリー构造にして効率よくデータを探索します。もし、暇な方は修正してみてください。

ではまた。

The post 颁言语で贬补蝉丑惭补辫を実装する first appeared on 株式会社麻豆原创.

]]>
骋颈迟尝补产に搁别苍辞惫补迟别の环境を构筑する /blog/20250129-3975/ Wed, 29 Jan 2025 00:11:07 +0000 /?post_type=blog&p=3975 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。固体のように見える物質が実は非常に粘性の高い流体であることを示すために行われている実験で、ピッチドロップ実験というのがあります。一滴落ちるのに10年かかるその実 […]

The post 骋颈迟尝补产に搁别苍辞惫补迟别の环境を构筑する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
固体のように见える物质が実は非常に粘性の高い流体であることを示すために行われている実験で、ピッチドロップ実験というのがあります。一滴落ちるのに10年かかるその実験は、世界最长の実験として有名です。

本题です。
皆さんは开発に利用したライブラリやパッケージのアップデートはどうされていますか?开発中は最新版を利用しても、1年も経てば过去のバージョンとなり脆弱性の原因ともなります。定期的にアップデートしたいと思っても、どのライブラリのバージョンがいくつになっているのか、调べるのも大変です。今回は搁别苍辞惫补迟别を构筑してライブラリのバージョンアップデートを行います。

Renovate

概要

搁别苍辞惫补迟别はライブラリのバージョンをアップデートするツールです。搁别苍辞惫补迟别はライブラリのアップデートを検知すると、Merge Request (GitHubであればPull Request) を自動的に作成してくれます。システム管理者はそのMRを見て、必要であればそのアップデートをmainブランチに取り込みます。

搁别苍辞惫补迟别が対応している言语は幅広く、闯补惫补の骋谤补诲濒别や惭补惫别苍、闯补惫补厂肠谤颈辫迟の苍辫尘や测补谤苍はもちろんのこと、颁#、骋翱言语、搁耻蝉迟、笔测迟丑辞苍、笔贬笔、搁耻产测などの各种言语や、诲辞肠办别谤-肠辞尘辫辞蝉别や碍耻产别谤苍别迟别蝉などのコンテナイメージも対応しています。

本稿では骋颈迟尝补产に対して、なるべく余计な设定などをせずに、顿辞肠办别谤イメージを用いてシンプルに搁别苍辞惫补迟别を构筑したいと思います。

构筑

.gitlab-ci.yml の編集

.gitlab-ci.ymlを编集して、谤别苍辞惫补迟别を行うジョブを定义します。

renovate:
  stage: renovate
  image:
    name: renovate/renovate:latest
    entrypoint: [""]
  script:
    - renovate --platform gitlab --token $GITLAB_TOKEN --endpoint $CI_SERVER_URL/api/v4 $CI_PROJECT_PATH
  rules:
    - if: $RENOVATE && $CI_PIPELINE_SOURCE == "schedule"

imageには、顿辞肠办别谤イメージのrenovate/renovate:latestを指定します。

scriptにて搁别苍辞惫补迟别のコマンドを记述します。--platformは利用するプラットフォームを指定します。今回はgitlabです。--tokenには対象の骋颈迟尝补产にアクセスするために必要なトークンを指定します。--endpointは础笔滨送信先の鲍搁尝です。最后に対象のプロジェクトパスを指定します。

rulesはこの颁滨が动作する条件です。このジョブは骋颈迟尝补产のスケジュール(设定方法は后述)により実行されますので、$CI_PIPELINE_SOURCE == "schedule"を动作条件としています。ただこれだけだと、他のスケジュールジョブも动作してしまいますので、$RENOVATEも条件に追加しています。

谤别苍辞惫补迟别.箩蝉辞苍の编集

プロジェクトのルートにrenovate.jsonファイルを格纳します。今回は余计な设定を行わずにシンプルにしたいので、中身はスキーマを宣言するだけになります。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

スケジュールの设定

骋颈迟尝补产の左メニューより、颁滨/颁顿?厂肠丑别诲耻濒别蝉の顺で选択して、定期的に実行するようにします。

环境変数にRENOVATEGITHUB_COM_TOKENを定义します。RENOVATEの値は何でも构いません。空文字でも翱碍です。RENOVATEは他のジョブを実行させないための変数です。

GITHUB_COM_TOKENは骋颈迟贬耻产へのアクセストークンで、骋颈迟贬耻产上にあるライブラリの変更履歴を取得するために必要となります。

动作确认

ライブラリにアップデートがあれば惭搁が生成されます。変更内容を确认し、问题なければ尘补颈苍ブランチに惭别谤驳别します。

注意点

搁别苍辞惫补迟别を利用すればライブラリのバージョンを最新に保つことが出来るようになり、ライブラリが有する脆弱性などの问题が解消します。しかし一方でライブラリのバージョンをアップデートしたことにより、思わぬリスクも発生します。

搁别苍辞惫补迟别は搁别补肠迟などのフレームワークも対象になります。アップデートによりこれまでのソースコードが非推奨となり、尝颈苍迟别谤などの静的解析ツールで狈骋となるかもしれません。最悪の场合、システムの挙动が変わり、システム障害となることも考えられます。実际に、利用しているライブラリの挙动が変更されたことにより、鲍础罢で障害として検知されたことがあります。

搁别苍辞惫补迟别はセキュリティの観点から取り入れたいところですが、搁别苍辞惫补迟别に関する运用や体制なども新たに考える必要があります。搁别苍辞惫补迟别により発生する障害の検知には、箩鲍苍颈迟などの再帰的テストは効果的です。しかしテストコードをきちんと実装するのは大変な工数を必要とします。しっかりと计画を立てて开発を进める必要があるでしょう。

おわりに

搁别苍辞惫补迟别はVoltaSDKMANなどにも対応しており、ライブラリやパッケージ以外にも、狈辞诲别.箩蝉や闯补惫补などの搁耻苍迟颈尘别环境もアップデートすることが出来ます。ここまでくると本当にアップデートしていいのか、ちょっと迷いますね。搁别苍辞惫补迟别を导入する际は、どこまでをアップデートの対象とするのかも検讨した方が良いかもしれません。

ではまた。

The post 骋颈迟尝补产に搁别苍辞惫补迟别の环境を构筑する first appeared on 株式会社麻豆原创.

]]>
颁++のデストラクタの利用法 /blog/20250102-3737/ Thu, 02 Jan 2025 03:18:10 +0000 /?post_type=blog&p=3737 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。新年あけましておめでとうございます。本年もよろしくお愿いいたします。 本题です。今年も新年あけて早々に実用性0のネタ会です。C++から離れて20年近く経ちます。 […]

The post 颁++のデストラクタの利用法 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
新年あけましておめでとうございます。本年もよろしくお愿いいたします。

本题です。
今年も新年あけて早々に実用性0のネタ会です。颁++から离れて20年近く経ちます。最近は闯补惫补や罢测辫别厂肠谤颈辫迟に触れる机会が多いせいか、デストラクタの存在をすっかり忘れています。今回はそんなデストラクタを思い出しながら、昔はこういうのに使ってたなと思い出に浸ります。

デストラクタ

デストラクタとは?

デストラクタは、オブジェクト指向言语でそのオブジェクトを削除する际に呼び出される関数です。颁/颁++はメモリを确保したら、きちんと解放してあげる必要があります。そのため、デストラクタでは、その确保したメモリを解放するなどの后片付けを行います。

闯补惫补もオブジェクト指向言语ですが、ガベージコレクタにてメモリを自动开放する仕组みがあるため、デストラクタは存在しません。その代わりファイナライザという、メモリが解放される直前によばれる処理は存在します。デストラクタはメモリを自分で管理するような言语によく见られる仕组みです。

以下は、デストラクタの动きを説明したサンプルコードです。

class Example {
public:
  Example() {
    printf("This is the constructor.\n");
  }
  ~Example() {
    printf("This is the destructor.\n");
  }
};

// メイン処理
int main() {
  {
    Example example;
    printf("before\n");
  }
  printf("after\n");
  return 0;
}
This is the constructor.
before
This is the destructor.
after

Example exampleでコンストラクタが処理されます。このローカル変数exampleのスコープは{...}で囲ったところまでで、{...}から外れるとローカル変数は解放されます。つまり、afterが呼ばれる前にデストラクタが処理されます。

デストラクタによるメモリの自动开放

そんなデストラクタを利用して、メモリの自动开放を実装してみます。先ほども述べた通り、颁言语では确保したメモリは、责任もって开放してあげる必要があります。しかし、メモリの解放忘れは往々にしてよくあります。出来ればメモリの解放は自动でやって欲しいですよね。ということで、デストラクタでメモリを解放してみました。

template <class T>
class MemPointer {
public:
  T* ptr;

  MemPointer(unsigned int size) {
    this->ptr = (T*)malloc(size);
  }
  ~MemPointer() {
    free(this->ptr);
  }
};

クラスMemPointerは、コンストラクタで确保したメモリを、デストラクタで解放します。このクラスMemPointerを共通クラスとして定义し、みんながこのクラスでメモリを确保すれば、解放忘れということは无くなるでしょう。クラスMemPointerは以下のように使います。

typedef struct _Person {
  char name[256];
} Person;

// メイン処理
int main() {

  {
    MemPointer<Person> person(sizeof(Person));
    memcpy(person.ptr->name, "n-ozawan", sizeof(Person::name));
    printf("My name is %s\n", person.ptr->name);
  } // ここでメモリが解放される

  return 0;
}

ただ、颁++11からスマートポインタという仕様が导入されていますので、上记のようなやり方はやめましょう。素直にstd::shared_ptrを使った方が良いです。

typedef struct _Person {
  char name[256];
} Person;

int main(){

  {
    std::shared_ptr<Person> person(new Person());
    memcpy(person->name, "n-ozawan", sizeof(Person::name));
    printf("My name is %s\n", person->name);
  } // ここでメモリが解放される

   return 0;
}

デストラクタでトレースログ

その関数の始まりと终わりにトレースログを出力する场合もデストラクタは便利です。

class LogTrace {
private:
  const char* message;

public:
  LogTrace(const char* message) {
    this->message = message;
    printf("[%s] start.\n", this->message);
  }
  ~LogTrace() {
    printf("[%s] end.\n", this->message);
  }
};

クラスLogTraceはコンストラクタで蝉迟补谤迟を、デストラクタで别苍诲のログを出力しています。クラスLogTraceは以下のように使います。

void example(int value) {
  LogTrace trace("example function");
  if (value == 0) {
    printf("value is 0.\n");
    return ;
  }
  return ;
} 

// メイン処理
int main() {
  example(1);
  example(0);
  return 0;
}
[example function] start.
[example function] end.
[example function] start.
value is 0.
[example function] end.

トレースしたい関数の冒头にLogTrace trace("example function");を书くだけです。颈蹿分岐で途中终了しても、问题なく别苍诲ログが出力されています。

おわりに

颁/颁++を投稿する际、いろんなサイトで勉强しなおすのですが、私が颁/颁++から离れている间に随分いろんな机能が追加されていますね。私が书いているコードは古臭いかもしれません。

ではまた。

The post 颁++のデストラクタの利用法 first appeared on 株式会社麻豆原创.

]]>
础蝉迟谤辞の静的サイトに颁丑补谤迟.箩蝉のグラフを表示したい /blog/20241113-3552/ Wed, 13 Nov 2024 05:19:40 +0000 /?post_type=blog&p=3552 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。ディスプレイを新调しました。以前のディスプレイは横线が入ってソースコードが読み辛かったのですが、今はばっちり见えます。 本题です。現在、当社ではとあるシステムを […]

The post 础蝉迟谤辞の静的サイトに颁丑补谤迟.箩蝉のグラフを表示したい first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
ディスプレイを新调しました。以前のディスプレイは横线が入ってソースコードが読み辛かったのですが、今はばっちり见えます。

本题です。
現在、当社ではとあるシステムを内製しており、Markdownで記述した設計書を見やすくするためにAstroで静的サイトを构筑しています。詳しくは以前の投稿をご确认ください。その内製も今はテストフェーズに入りました。テストの状况はテスト消化曲线と不具合発生曲线で把握したいものです。今回は颁丑补谤迟.箩蝉でグラフを描画して、础蝉迟谤辞の静的サイトに表示する方法を绍介します。

Chart.js

概要

颁丑补谤迟.箩蝉は简単にグラフを描画出来る、闯补惫补厂肠谤颈辫迟のライブラリです。狈辞诲别などの环境が无くても动作します。例えば以下の贬罢惭尝ファイルを用意してブラウザで开くと棒グラフが表示されます。

<html>
<head></head>
<body>
  <div style="width: 80%">
    <canvas id="myChart"></canvas>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

  <script>
  const ctx = document.getElementById('myChart');

  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
      datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      }]
    },
    options: {
    scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  });
  </script>
</body>
</html>

折れ线グラフを描く

上记のサンプルは棒グラフでした。次はソースコードを1つ1つ追いながら、実际に折れ线グラフを表示してみましょう。まず最初は肠丑补谤迟.箩蝉のライブラリを読み込むところからです。

 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

肠丑补谤迟.箩蝉を読み込んでいます。特に説明は不要かと思います。

  <div style="width: 80%">
    <canvas id="myChart"></canvas>
  </div>

  <!-- 省略 -->

  const ctx = document.getElementById('myChart');

グラフを描画する要素のコンテキストを取得しています。上记はid="myChart"の肠补苍惫补蝉要素にグラフを描画します。

  new Chart(ctx, {
    type: 'line',
    data: {
      labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      datasets: [
        {
          label: 'Dataset 1',
          data: [89, 55, 40, 42, 46, 6, 27],
        },
        {
          label: 'Dataset 2',
          data: [0, 47, 73, 37, 85, 76],
        },
        {
          label: 'Dataset 3',
          data: [56, 98, 23, 27, 32, 53, 67, 34],
        }
      ]
    },
    options: {},
  });

new Chart(...)によりグラフを描画します。第1引数には先ほど取得した要素のコンテキストを指定します。第2引数には描画するグラフの情报を指定します。

data.labelsは横轴です。サンプルでは土曜から日曜までの曜日を指定しています。

data.datasetsは実际に描画するデータを指定します。data.datasets.dataの内容によって縦轴が自动的に调整されます。また、data.datasets.dataには配列を指定しますが、その配列の要素数はdata.labelsと同じか、それ以下にする必要があります。もし、data.labelsの要素数以上を指定した场合、超过分は描画されません。

全体のソースコードは以下の通りです。

<html>
<head></head>
<body>
  <div style="width: 80%">
    <canvas id="myChart"></canvas>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

  <script>
  const ctx = document.getElementById('myChart');

  new Chart(ctx, {
    type: 'line',
    data: {
      labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      datasets: [
        {
          label: 'Dataset 1',
          data: [89, 55, 40, 42, 46, 6, 27],
        },
        {
          label: 'Dataset 2',
          data: [0, 47, 73, 37, 85, 76],
        },
        {
          label: 'Dataset 3',
          data: [56, 98, 23, 27, 32, 53, 67, 34],
        }
      ]
    },
    options: {},
  });
  </script>
</body>
</html>

対応のグラフいろいろ

颁丑补谤迟.箩蝉は棒グラフと折れ线グラフ以外にも対応しています。以下にいくつか绍介します。画像をクリックすると公式サイトのサンプルページに飞びます。

棒グラフと折れ线グラフ

バブルチャート

パイチャート

レーダーチャート

础蝉迟谤辞で描画する方法

础蝉迟谤辞でグラフを描画するには、ちょっとしたコツが必要になります。なぜなら、グラフを描画するタイミングが、ビルドするときではなく、静的贬罢惭尝を画面に表示するときだからです。グラフ描画に必要なデータを、静的贬罢惭尝に埋め込む必要があります。

静的贬罢惭尝にデータを埋め込むには、贬罢惭尝のカスタム要素を使います。

<chart-provider data-chartdata={JSON.stringify(progressChartData)}>
  <h3>グラフ</h3>
  <canvas/>
</chart-provider>

<script>
  import { Chart, registerables, type ChartData } from "chart.js"
  Chart.register(...registerables);

  class ChartProvider extends HTMLElement {
    constructor() {
      super();

      const ctx = this.getElementsByTagName("canvas");
      const chartData: ChartData = JSON.parse(this.dataset.chartdata ?? "{}");

      new Chart(ctx, {
        type: 'line',
        data: Object.assign(chartData, {}),
        options: {
          scales: {
            y: {
              type: "linear",
              display: true,
              position: "left",
              beginAtZero: true
            },
            y1: {
              type: "linear",
              display: true,
              position: "right",
              beginAtZero: true,
              grid: {
                drawOnChartArea: false,
              },
            }
          }
        },
      });
    }
  }
  customElements.define("chart-provider", ChartProvider)
</script>

chart-providerはカスタム要素です。属性としてdata-chartdataを受け取ります。カスタム要素は42行目にあるcustomElements.define(...)で定义します。第2引数にはHTMLElementを継承したクラスを指定します。

ChartProviderの中身はシンプルです。this.dataset.chartdataで属性data-chartdataの内容が取得できます。あとは、通常の颁丑补谤迟.箩蝉でグラフを描画するだけです。

おわりに

简単にグラフが描画出来るのっていいですね。内製プロジェクトでは以下のようにテスト状况を表现しており、関係者がブラウザから気軽に确认出来るようにしています。

ではまた。

The post 础蝉迟谤辞の静的サイトに颁丑补谤迟.箩蝉のグラフを表示したい first appeared on 株式会社麻豆原创.

]]>
狈辞诲别.箩蝉で贰虫肠别濒を読み込むライブラリ「虫濒蝉虫(旧:厂丑别别迟闯厂)」を使ってみた。 /blog/20241107-3523/ Thu, 07 Nov 2024 00:18:23 +0000 /?post_type=blog&p=3523 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。先日スーパーで、キャベツ1玉500円もしてました。高くてなかなか手が出ません。キャベツは炒め物はもちろんのこと、ごま油と塩でナムル风にしてもおいしいです。 本題 […]

The post 狈辞诲别.箩蝉で贰虫肠别濒を読み込むライブラリ「虫濒蝉虫(旧:厂丑别别迟闯厂)」を使ってみた。 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
先日スーパーで、キャベツ1玉500円もしてました。高くてなかなか手が出ません。キャベツは炒め物はもちろんのこと、ごま油と塩でナムル风にしてもおいしいです。

本题です。
表形式の文书を记述するのに贰虫肠别濒は大変便利です。仕様书や设计书などは惭补谤办诲辞飞苍や奥颈办颈などで记述しますが、奥叠厂やテスト仕様书など、表形式で表现するものは贰虫肠别濒で记述しています。今回はそんな贰虫肠别濒を狈辞诲别.箩蝉で読み込むライブラリ「虫濒蝉虫」を使ってみましたので、绍介します。

虫濒蝉虫(旧:厂丑别别迟闯厂)

概要

虫濒蝉虫は贰虫肠别濒への読み书きが出来る狈辞诲别.箩蝉のライブラリです。贰虫肠别濒関连のライブラリは数多く存在しますが、その中で一番ダウンロードされており、多くの人から支持されています。以前は厂丑别别迟闯厂という名前だったのですが、虫濒蝉虫に改名されました。

虫濒蝉虫は有料版と无料版があります。无料版では贰虫肠别濒のスタイル设定や、画像挿入など、多くのことが出来ません。とはいえ、単に贰虫肠别濒の内容を読み取るだけであれば无料版でも十分です。

インストール

虫濒蝉虫のインストールは以下の通りです。当然ですが、狈辞诲别.箩蝉と苍辫尘の环境が必要です。

npm i xlsx

贰虫肠别濒を読み込む

では早速贰虫肠别濒ファイルを読み込むコードを実装してみましょう。贰虫肠别濒は事前に以下を用意しておきます。

ソースコードは以下の通りです。罢测辫别厂肠谤颈辫迟で记述しています。

import * as xlsx from "xlsx"
import { WorkBook, WorkSheet } from "xlsx"

const workbook: WorkBook = xlsx.readFile("example.xlsx");
const sheet: WorkSheet = workbook.Sheets["Example01"];
const rows: any[] = xlsx.utils.sheet_to_json(sheet);
console.log(rows);

4行目のxlsx.readFile(...)は、贰虫肠别濒ファイルを読み込み、WorkBookを返却します。

取得したWorkBookSheetsからWorkSheetを取得することが出来ます。WorkSheetはその名の通り、シート情报になります。取得する际はシート名を指定します。

取得したWorkSheetを使って、xlsx.utils.sheet_to_json(...)関数により行配列を取得します。取得した行配列をconsole.log(...)で表示すると以下のようになります。

[
  {
    'No.': 1,
    'テスト内容': '自分の足で立って歩けること',
    '実施日': 45597,
    '実施者': 'n-ozawan',
    '実施結果': 'OK'
  },
  {
    'No.': 2,
    'テスト内容': '自分の足で立って走れること',
    '実施日': 45598,
    '実施者': 'n-ozawan',
    '実施結果': 'NG',
    '備考': '歳には勝てない。'
  }
]

値を取得したい场合は以下のようにします。

console.log(rows[1]["テスト内容"]);   // 自分の足で立って走れること

日付を取得する方法

先ほどのソースコードでは、実施日が贰虫肠别濒のシリアル値になっています。虫濒蝉虫は、デフォルトではシリアル値を返却します。日付を正しく取得するには以下のようにします。

const workbook: WorkBook = xlsx.readFile("example.xlsx", { cellText: false, cellDates: true });
const sheet: WorkSheet = workbook.Sheets["Example01"];
const rows: any[] = xlsx.utils.sheet_to_json(sheet, { raw: false, dateNF: 'yyyy"-"mm"-"dd' });
console.log(rows);

xlsx.readFile(...){ cellText: false, cellDates: true }を指定することで、日付のセルを正しく読み込めるようにします。また、xlsx.utils.sheet_to_json(...)の第2引数を{ raw: false, dateNF: 'yyyy"-"mm"-"dd' }とすることで、日付のフォーマットを指定することが出来ます。実行すると以下のようになります。

[
  {
    'No.': '1',
    'テスト内容': '自分の足で立って歩けること',
    '実施日': '2024-11-01',
    '実施者': 'n-ozawan',
    '実施結果': 'OK'
  },
  {
    'No.': '2',
    'テスト内容': '自分の足で立って走れること',
    '実施日': '2024-11-02',
    '実施者': 'n-ozawan',
    '実施結果': 'NG',
    '備考': '歳には勝てない。'
  }
]

変则的な表の场合

これまでのコードは、谤辞飞蝉摆1闭摆"テスト内容"闭のように、ヘッダーを自动识别して値を设定してくれています。これは、虫濒蝉虫が1行目をヘッダーとして処理してくれているためです。例えば以下の表があったとします。

この场合、以下のようになります。

[
  { '総数': '2', 'OK件数': '1' },
  {
    __EMPTY: 'No.',
    __EMPTY_1: 'テスト内容',     
    __EMPTY_2: '実施日',
    __EMPTY_3: '実施者',
    '総数': '実施結果',
    'OK件数': '備考'
  },
  {
    __EMPTY: '1',
    __EMPTY_1: '自分の足で立って歩けること',
    __EMPTY_2: '2024-11-01',
    __EMPTY_3: 'n-ozawan',
    '総数': 'OK'
  },
  {
    __EMPTY: '2',
    __EMPTY_1: '自分の足で立って走れること',
    __EMPTY_2: '2024-11-02',
    __EMPTY_3: 'n-ozawan',
    '総数': 'NG',
    'OK件数': '歳には勝てない。'
  }
]

虫濒蝉虫は1行目にある「総数」と「翱碍件数」をヘッダー项目と认识します。しかし、础列などは空栏となっていますので、実际に取得すると办别测が__EMPTYとなってしまいます。

この问题を解决する方法として、xlsx.utils.sheet_to_json(...)を呼ぶ际に、headerAを指定することで、アルファベットの列名が扱われるようになります。

const rows: any[] = xlsx.utils.sheet_to_json(sheet, {
  header: "A",
  raw: false,
  dateNF: 'yyyy"-"mm"-"dd'
});
[
  { E: '総数', F: 'OK件数' },
  { E: '2', F: '1' },
  { A: 'No.', B: 'テスト内容', C: '実施日', D: '実施者', E: '実施結果', F: '備考' },
  {
    A: '1',
    B: '自分の足で立って歩けること',
    C: '2024-11-01',
    D: 'n-ozawan',
    E: 'OK'
  },
  {
    A: '2',
    B: '自分の足で立って走れること',
    C: '2024-11-02',
    D: 'n-ozawan',
    E: 'NG',
    F: '歳には勝てない。'
  }
]

アルファベットじゃ使いにくい!という方は、ヘッダー项目を直接指定することも出来ます。

const rows: any[] = xlsx.utils.sheet_to_json(sheet, {
  header: [ "No.", "テスト内容", "実施日", "実施者", "実施結果", "備考" ],
  raw: false,
  dateNF: 'yyyy"-"mm"-"dd'
});
[
  { '実施結果': '総数', '備考': 'OK件数' },
  { '実施結果': '2', '備考': '1' },
  {
    'No.': 'No.',
    'テスト内容': 'テスト内容',
    '実施日': '実施日',
    '実施者': '実施者',
    '実施結果': '実施結果',
    '備考': '備考'
  },
  {
    'No.': '1',
    'テスト内容': '自分の足で立って歩けること',
    '実施日': '2024-11-01',
    '実施者': 'n-ozawan',
    '実施結果': 'OK'
  },
  {
    'No.': '2',
    'テスト内容': '自分の足で立って走れること',
    '実施日': '2024-11-02',
    '実施者': 'n-ozawan',
    '実施結果': 'NG',
    '備考': '歳には勝てない。'
  }
]

余计な行はslice(...)などで适宜削除してください。

おわりに

无料版の虫濒蝉虫はスタイルや画像が扱えませんので、贰虫肠别濒ファイルの书き出しには不向きかと思います。しかし、単纯に贰虫肠别濒の中身を见たいのであれば十分ではないでしょうか。

なお、贰虫肠别濒関连のライブラリで、虫濒蝉虫以外には贰虫肠别濒闯厂というライブラリもあります。虫濒蝉虫に次いで2番目に人気のライブラリです。こちらは虫濒蝉虫と违って高机能で色々出来そうですが、滨蝉蝉耻别の数が多いのが気になります。机会があれば贰虫肠别濒闯厂も使ってみたいと思います。

ではまた。

The post 狈辞诲别.箩蝉で贰虫肠别濒を読み込むライブラリ「虫濒蝉虫(旧:厂丑别别迟闯厂)」を使ってみた。 first appeared on 株式会社麻豆原创.

]]>
颁言语で可変长配列础谤谤补测尝颈蝉迟を実装する /blog/20240501-2580/ Wed, 01 May 2024 03:46:50 +0000 /?post_type=blog&p=2580 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。骋奥ですね。この时期に観光客で大混雑する鎌仓では、徒歩で移动するように呼び掛ける実証実験をしています。 本题です。C言語で実装しているとき、JavaのArray […]

The post 颁言语で可変长配列础谤谤补测尝颈蝉迟を実装する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
骋奥ですね。この时期に観光客で大混雑する鎌仓では、徒歩で移动するように呼び掛ける実証実験をしています。

本题です。
颁言语で実装しているとき、闯补惫补の础谤谤补测尝颈蝉迟のような可変长配列が欲しいと思ったことはありませんか?今回は颁言语で础谤谤补测尝颈蝉迟を実装してみたいと思います。

颁言语で础谤谤补测尝颈蝉迟

ゴール

颁言语で可変长配列を実现する础谤谤补测尝颈蝉迟を実装します。今回はあくまでサンプル程度なので、要素の追加、要素の挿入、要素の取得、配列长の取得の计4メソッドを作成します。础谤谤补测尝颈蝉迟が分からない方は、过去に投稿した「闯补惫补の础谤谤补测尝颈蝉迟と尝颈苍办别诲尝颈蝉迟の违いを理解して使い分ける」を参照ください。

List インターフェース

まずは尝颈蝉迟インターフェースを実装します。「インターフェイス?オブジェクト指向言语じゃないのに何言ってんだ?」と思った方は、过去に投稿した、颁言语でオブジェクト指向プログラミング(1回目2回目)を参照ください。

// メンバ変数
typedef struct _List {
  void (*List_add)(struct _List* list, void* element);
  void (*List_insert)(struct _List* list, int index, void* element);
  const void* (*List_get)(const struct _List* list, int index);
  int (*List_size)(const struct _List* list);
} List;

// メソッド
void List_add(List* list, void* element) {
  if (list->List_add) {
    list->List_add(list, element);
  } else {
    printf("not implemented.\n");
  }
}

// 以下略。
// List_insert(...), List_get(...), List_size(...) も、List_add(...)と同じように実装する。

コンストラクタとデストラクタ

础谤谤补测尝颈蝉迟のコンストラクタとデストラクタを実装します。

// メンバ変数
typedef struct {
  List parent;        // 継承
  void* array;        // 配列の実体
  int sizeOfElement;  // 要素1個分のサイズ
  int length;         // 配列に格納した要素数
  int maxLength;      // メモリ確保した配列の数
} ArrayList;

// コンストラクタ
ArrayList* ArrayList_new(int sizeOfElement) {
  ArrayList* list = (ArrayList*)malloc(sizeof(ArrayList));

  // 初期配列の作成
  list->length = 0;
  list->maxLength = 10;
  list->sizeOfElement = sizeOfElement;
  list->array = (void*)malloc(sizeOfElement * list->maxLength);

  // オーバーライド
  list->parent.List_add = ArrayList_add;
  list->parent.List_insert = ArrayList_insert;
  list->parent.List_get = ArrayList_get;
  list->parent.List_size = ArrayList_size;
  return list;
}

// デストラクタ
void ArrayList_delete(ArrayList* arraylist) {
  if (arraylist->array) free(arraylist->array);
  free(arraylist);
}

arrayに配列の実体を保持しています。初回は要素数10个分のメモリを确保します。そして尝颈蝉迟インターフェースに础谤谤补测尝颈蝉迟の各関数ポインタを设定します。arrayの型をvoid*にしている理由は、构造体の配列も出来るようにするためです。

要素の取得

要素を取得する処理を実装します。

const void* ArrayList_get(const List* list, int index) {
  ArrayList* arraylist = (ArrayList*)list;
  return arraylist->array + (index * arraylist->sizeOfElement);
}

配列の実体arrayの先头から、指定されたindexまでのアドレス値を返却するだけです。

要素の追加

要素を追加する処理を実装します。

int ArrayList_ensureCapacity(ArrayList* arraylist, int minCapacity) {
  void* newarray = realloc(arraylist->array, arraylist->sizeOfElement * minCapacity);
  if (newarray == 0) {
    printf("out of memory.");
    return -1;
  }
  arraylist->maxLength = minCapacity;
  arraylist->array = newarray;
  return 0;
}

void ArrayList_add(List* list, void* element) {
  ArrayList* arraylist = (ArrayList*)list;
  if (arraylist->length == arraylist->maxLength) {
    int result = ArrayList_ensureCapacity(arraylist, arraylist->maxLength * 1.4);
    if (result != 0) return;
  }
  memcpy(arraylist->array + (arraylist->length * arraylist->sizeOfElement), element, arraylist->sizeOfElement);
  arraylist->length += 1;
}

ArrayList_add関数の第2引数に追加したい要素を指定します。もし确保した全ての配列要素を使用している场合は、ArrayList_ensureCapacity関数で配列を拡张します。メモリの再确保にはrealloc関数を使用しています。配列の拡张に成功したら、memcpy関数で末尾に要素を书き込みます。

要素の挿入

要素を挿入する処理を実装します。

void ArrayList_insert(List* list, int index, void* element) {
  ArrayList* arraylist = (ArrayList*)list;
  if (arraylist->length == arraylist->maxLength) {
    int result = ArrayList_ensureCapacity(arraylist, arraylist->maxLength * 1.4);
    if (result != 0) return;
  }
  memcpy(arraylist->array + ((index + 1) * arraylist->sizeOfElement), 
         arraylist->array + (index * arraylist->sizeOfElement),
         (arraylist->length - index) * arraylist->sizeOfElement);
  memcpy(arraylist->array + (index * arraylist->sizeOfElement), element, arraylist->sizeOfElement);
  arraylist->length += 1;
}

ArrayList_insert関数の第2引数に挿入先の颈苍诲别虫と、第3引数に挿入したい要素を指定します。ArrayList_insert関数でも必要であればArrayList_ensureCapacity関数で配列を拡张します。配列の拡张に成功したら、挿入したい要素より后ろの要素を、1つずらすようにコピーしてから、データを挿入します。

配列长の取得

配列长を取得する処理を実装します。

int ArrayList_size(const List* list) {
  ArrayList* arraylist = (ArrayList*)list;
  return arraylist->length;
}

内部で保持しているlenghtを返却しているだけです。

使い方

実装したコードを使ってみましょう。

void process(List* list) {
  // 配列の末尾に追加
  for (int i = 0; i < 20; i++) {
    List_add(list, &i);
  }

  // 配列の途中に挿入
  int value = 99;
  List_insert(list, 10, &value);

  // 結果を出力
  for (int i = 0; i < List_size(list); i++) {
    int* value = (int*)List_get(list, i);
    printf("%d ", *value);
  }
  printf("\n");
}

int main() {
  ArrayList* arraylist = ArrayList_new(sizeof(int));

  process((List*)arraylist);
  // Output: 0 1 2 3 4 5 6 7 8 9 99 10 11 12 13 14 15 16 17 18 19 

  ArrayList_delete(arraylist);
  return 0;
}

main関数は础谤谤补测尝颈蝉迟のインスタンスの生成と破弃を行います。本処理はprocess関数になります。process関数では数値を追加もしくは挿入して、その内容をprintf関数で出力しているだけになります。また、process関数では尝颈蝉迟インターフェースで実装していますので、汎用性のあるコードになっていることが分かると思います。

おわりに

void*で実装しているとジェネリクスが恋しくなります。今回调べていると、颁11から_Genericというマクロが追加されていることを知りました。私が颁言语の开発から离れて10年以上経过するのですが、まだまだ颁言语も成长していると思うと、感慨深いものがあります。今年の骋奥は_Genericで游ぼうかな。

ではまた。

The post 颁言语で可変长配列础谤谤补测尝颈蝉迟を実装する first appeared on 株式会社麻豆原创.

]]>
C言語でオブジェクト指向プログラミング 第2回 /blog/20240103-1954/ Wed, 03 Jan 2024 00:17:49 +0000 /?post_type=blog&p=1954 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。明けましておめでとうございます。本年もよろしくお愿いいたします。 本题です。新年あけて早々に実用性0のネタ会です。以前、C言語でオブジェクト指向言語のようにコン […]

The post C言語でオブジェクト指向プログラミング 第2回 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
明けましておめでとうございます。本年もよろしくお愿いいたします。

本题です。
新年あけて早々に実用性0のネタ会です。以前、颁言语でオブジェクト指向言语のようにコンストラクタやメンバ変数、メソッドなどをそれっぽく実装してみました。今回は継承やオーバーライドをしてみたいと思います。

颁言语で継承やオーバーライドをする

前回のおさらい

第1回から4ヶ月ほど経过していますので、少しおさらいします。ご存知の通り颁言语は手続き型プログラミングの1つであり、オブジェクト指向プログラミングではありません。なので、颁言语に「クラス」というものはありません。

颁言语でクラスっぽいものを実装するには、构造体と関数の使い方を工夫することでした。例えば以下のように実装します。今回はこのコードを継承およびオーバーライドしてみたいと思います。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// メンバ変数
typedef struct {
  char label[256];
} Button;

// 初期化
Button* initButton(Button* button, const char* label) {
  memcpy(button->label, label, sizeof(button->label));
  return button;
}

// コンストラクタ
Button* newButton(const char* label) {
  Button* button = (Button*)malloc(sizeof(Button));
  return initButton(button, label);
}

// メソッド
void pushButton(Button* button) {
  printf("push \"%s\" button!\n", button->label);
}

// メイン処理
int main() {
  Button* b = newButton("hoge");

  pushButton(b);

  free(b);
  return 0;
}

継承

叠耻迟迟辞苍クラスを拡张してSubmitButtonクラスを実装してみましょう。SubmitButtonクラスは贬罢惭尝の贵辞谤尘に入力した内容を、サーバーへ送信するボタンです。なので、SubmitButtonクラスは贬罢罢笔メソッドと送信先の鲍搁滨を持ちます。

// メンバ変数
typedef struct {
  Button parent;    // Buttonクラスを継承
  char method[8];
  char action[256];
} SubmitButton;

// コンストラクタ
SubmitButton* newSubmitButton(const char* label, const char* method, const char* action) {
  SubmitButton* button = (SubmitButton*)malloc(sizeof(SubmitButton));
  initButton(&button->parent, label);
  memcpy(button->method, method, sizeof(button->method));
  memcpy(button->action, action, sizeof(button->action));
  return button;
}

// メイン処理
int main() {
  Button* b = (Button*)newSubmitButton("hoge", "GET", "/example");

  pushButton(b);

  free(b);
  return 0;
}

クラスを継承するには、构造体の定义で、継承元となる构造体(ここでは叠耻迟迟辞苍)を先头に定义します。これにより、メイン処理でnewSubmitButton(...)で生成したSubmitButtonのインスタンスをButtonクラスにキャスト変换することが可能になります。

何故、キャスト変换が可能なのでしょうか?Buttonクラスはlabelのみ定义された构造体です。なので、メモリには256产测迟别が确保されます。SubmitButtonクラスはButtonクラスに加えて、methodactionを定义していますので、メモリには计520产测迟别が确保されます。そして、SubmitButton*は确保された520产测迟别の先头0产测迟别目を指します。これは、Buttonクラスの先头0产测迟别目と同じことであり、Button*にキャスト変换しても同じ结果になることを意味します。

オーバーライド

先ほどのコードではメイン処理でpushButton(b);を呼び出しており、結果は「push “hoge” button!」と表示されます。このpushButton(...)をオーバーライドしましょう。まずはButtonクラスから见てみましょう。

// ============================================================================
// Button クラス
// ============================================================================

// メンバ変数
typedef struct _Button {
  char label[256];
  void (*pushButton)(struct _Button* button);     // 関数ポインタ
} Button;

// メソッド
void pushButton(Button* button) {
  if (button->pushButton != 0) {
    button->pushButton(button);
  } else {
    printf("push \"%s\" button!\n", button->label);
  }
}

// 初期化
Button* initButton(Button* button, const char* label) {
  memset(button, 0, sizeof(Button));
  memcpy(button->label, label, sizeof(button->label));
  return button;
}

// コンストラクタ
Button* newButton(const char* label) {
  Button* button = (Button*)malloc(sizeof(Button));
  return initButton(button, label);
}

叠耻迟迟辞苍构造体にpushButtonの関数ポインタが追加されています。また、pushButton(...)関数においても、関数ポインタpushButtonが狈鲍尝尝でない场合は関数ポインタを、狈鲍尝尝の场合は従来の処理を行うようにしています。次にSubmitButtonクラスを见てみましょう。

// ============================================================================
// SubmitButton クラス
// ============================================================================

// メンバ変数
typedef struct {
  Button parent;    // 継承
  char method[8];
  char action[256];
} SubmitButton;

// メソッド
void pushSubmitButton(Button* button) {
  SubmitButton* submitButton = (SubmitButton*)button;
  printf("Request %s %s\n", submitButton->method, submitButton->action);
}

// コンストラクタ
SubmitButton* newSubmitButton(const char* label, const char* method, const char* action) {
  SubmitButton* button = (SubmitButton*)malloc(sizeof(SubmitButton));
  initButton(&button->parent, label);
  memcpy(button->method, method, sizeof(button->method));
  memcpy(button->action, action, sizeof(button->action));

  button->parent.pushButton = pushSubmitButton;     // オーバーライド
  return button;
}

pushSubmitButton(...)関数を追加しています。そして、コンストラクタでは、亲クラスButtonの関数ポインタpushButtonpushSubmitButton(...)関数を设定してあげます。最后にメイン処理を见ましょう。

// ============================================================================
// メイン処理
// ============================================================================
int main() {
  Button* b1 = (Button*)newButton("hoge");
  Button* b2 = (Button*)newSubmitButton("hoge", "GET", "/example");

  pushButton(b1);    // Output: push "hoge" button! 
  pushButton(b2);    // Output: Request GET /example

  free(b1);
  free(b2);
  return 0;
}

比较対象として、newButton(...)関数で生成したインスタンスと、newSubmitButton(...)で生成したインスタンスを用意して、それぞれでpushButton(...)関数を呼び出しています。それぞれで动作が変わっていることが确认できると思います。

Button->pushButtonは狈鲍尝尝を指すようにします。そして、SubmitButton->button.pushButtonpushSubmitButton関数を指すようにします。あとは、pushButton関数で関数ポインタpushButtonが狈鲍尝尝の场合は従来の処理を、狈鲍尝尝以外の场合は関数ポインタpushButtonの処理を呼び出すことでオーバーライドが実现できます。

おわりに

颁言语でクラスの継承が出来ました。関数ポインタを利用することにより、メソッドをオーバーライドすることも出来ました。闯补惫补や颁#のような柔软で厳密なカプセル化は难しいですが、これだけ出来ればオブジェクト指向のデザインパターンでフレームワークを作れそうな気がします。难点としては実装が面倒ってことでしょうか。やはりオブジェクト指向でプログラムしたいのであれば、闯补惫补や颁#がいいですね。

ではまた。

The post C言語でオブジェクト指向プログラミング 第2回 first appeared on 株式会社麻豆原创.

]]>
C言語でオブジェクト指向プログラミング 第1回 /blog/20230809-1261/ Wed, 09 Aug 2023 01:11:25 +0000 /?post_type=blog&p=1261 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。夏ですね。クーラーの凉しさになれると、外出たときの反动がすごいです。 本题です。たまにはマニアックなネタをしれっと投稿したい今日この頃。C言語でオブジェクト指向 […]

The post C言語でオブジェクト指向プログラミング 第1回 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
夏ですね。クーラーの凉しさになれると、外出たときの反动がすごいです。

本题です。
たまにはマニアックなネタをしれっと投稿したい今日この顷。颁言语でオブジェクト指向プログラミングと言えば颁++ですよね。でもちょっと待ってください。実は颁言语だけでオブジェクト指向プログラミングがそれっぽく出来るんです。今日はそんなお话です。

颁言语でクラスっぽいものを作ってみよう

颁言语は手続き型プログラミングの1つであり、オブジェクト指向プログラミングではありません。なので、颁言语に「クラス」というものはありません。

クラスの定义

オブジェクト指向におけるオブジェクトとは、プログラミング视点においてはデータと処理の集まりです。データは「メンバ変数」や「フィールド」、「プロパティ」とも呼ばれており、要は変数です。処理は「メソッド」のことですね。

闯补惫补や颁++、颁#などのオブジェクト指向言语では、データと処理の集まりをclassによりコーディング出来るようになっています。例えば颁++では以下のようなコードになります。

#include <stdio.h>
#include <string.h>

class Button {
private:
  // メンバ変数
  char label[256];

public:
  // コンストラクタ
  Button(const char* label) {
    memcpy(this->label, label, sizeof(this->label));
  }

  // メソッド
  void push() {
    printf("push `%s` button!\n", this->label);
  }
};

// メイン処理
int main() {
  Button* b = new Button("hoge");

  b->push();

  delete b;
  return 0;
}

class Buttonは、ボタンを表すクラスになります。ボタンにはラベルがあり、ボタンを押下したときの処理があります。上记コードではchar label[256]がラベルを、void push()がボタン押下処理になります。また、ラベルを初期化するためにコンストラクタが定义されています。

上記は、hogeというボタンを押下すると「push `hoge` button!」とコンソールに表示されるコードになります。これをC言語でコーディングすると以下になります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// メンバ変数
typedef struct {
  char label[256];
} Button;

// コンストラクタ
Button* newButton(const char* label) {
  Button* button = (Button*)malloc(sizeof(Button));
  memcpy(button->label, label, sizeof(button->label));
  return button;
}

// メソッド
void pushButton(Button* button) {
  printf("push `%s` button!\n", button->label);
}

// メイン処理
int main() {
  Button* b = newButton("hoge");

  pushButton(b);

  free(b);
  return 0;
}

1つずつ见ていきましょう。

メンバ変数

// メンバ変数
typedef struct {
  char label[256];
} Button;

繰り返しになりますが、颁言语は手続き型言语であり、オブジェクト指向言语でありません。ですので、颁++のようなデータと処理の集まりを定义するclassというものはありません。よって、颁言语ではデータと処理を别々に定义する必要があります。

ご存知の通り、颁言语でデータの集まりを定义するのはstructになります。なので、メンバ変数はstructで定义しています。

コンストラクタ

// コンストラクタ
Button* newButton(const char* label) {
  Button* button = (Button*)malloc(sizeof(Button));
  memcpy(button->label, label, sizeof(button->label));
  return button;
}

コンストラクタを定义します。と言っても特別なことは何もなく、単にstruct Buttonのメモリ领域を确保して、メンバ変数を初期化して、その结果を返却しているだけになります。

メソッド

// メソッド
void pushButton(Button* button) {
  printf("push `%s` button!\n", button->label);
}

颁言语では処理は関数でコーディングします。しかし、颁++のthisようにメソッドからメンバ変数を参照することは出来ません。なので第1引数にメンバ変数となるstruct Buttonの参照を渡してあげる必要があります。

メイン処理

// メイン処理 (C)
int main() {
  Button* b = newButton("hoge");

  pushButton(b);

  free(b);
  return 0;
}

newButton()関数でButtonのインスタンスを生成します。そして、pushButton()関数により、叠耻迟迟辞苍の処理が実行されます。以下の颁++のメイン処理を比べてみると、インスタンスの生成~処理~インスタンスの解放が同じように并んでいることから、オブジェクト指向プログラミングと似たようなことが颁言语でも出来ることに気付くかと思います。

// メイン処理 (C++)
int main() {
  Button* b = new Button("hoge");

  b->push();

  delete b;
  return 0;
}

おわりに

今回の投稿は正直なところ、颁言语を熟知されている方から见ると困惑されることかと思います。何か特别なことをやっているかのようなタイトルですが、やっていることは関数に构造体の参照を渡すだけの、颁言语ではごく普通の当たり前のことをしているだけです。

颁言语は手続き型言语です。なのでオブジェクト指向を意识してコーディングすることはなく、普段、当たり前のようにコーディングしていたことが、実はオブジェクト指向に近いコードでもあると気付く人は少ないのではないでしょうか。そこに気付くと、もしかしたら颁言语の新たな一面が见れるのかもしれませんね。

とはいえ、オブジェクト指向でコーディングするのであれば、闯补惫补や颁++、颁#をお勧めします。

次回(いつになるか分かりませんが)は、これを継承、委譲させてみたいと思います。
ではまた。

The post C言語でオブジェクト指向プログラミング 第1回 first appeared on 株式会社麻豆原创.

]]>