Возможно, я упускаю что-то простое, но мне трудно убедить компилятор Rust, что этот писатель let mut writer = BufWriter::new(file); можно использовать в привязке к трейту F: Fn(Write) -> Result<()>. Например, ниже работает:
fn write_using_writer<F>(file: &Path, f: F) -> Result<()>
where
F: Fn(&mut BufWriter<File>) -> Result<()>,
{
let file = File::create(file)?;
let mut writer = BufWriter::new(file);
let _ = f(&mut writer)?;
writer.flush()?;
Ok(())
}
fn write_to_file(data: MySerializableStruct) -> Result<()> {
let path = Path::new("./output.json");
write_using_writer(path, |writer: &mut BufWriter<File>|
serde_json::to_writer(writer, data).map_err(|e| Error::SerdeJson { err: e }))
}
Я хотел обобщить это для некоторого Writer, т. е. я хочу, чтобы потребитель на месте вызова использовал какой-то Writer, не обязательно зная его конкретную реализацию.
Пытаясь успокоить компилятор Rust, я сделал следующее:
fn write_using_writer_a<W, F>(file: &Path, f: F) -> Result<()>
where
W: Write,
F: Fn(W) -> Result<()>,
{
let _ = ensure_dir(file)?;
let file = File::create(file)?;
let mut writer = BufWriter::new(file);
let _ = f(&mut writer)?;
writer.flush()?;
Ok(())
}
Однако компилятор выдает мне следующее сообщение об ошибке:
mismatched types
expected type parameter `W`
found mutable reference `&mut std::io::BufWriter<std::fs::File>`
Что мне не хватает?
Я попробовал заменить
F: Fn(W) -> Result<()>,
с
F: FnMut(W) -> Result<()>,
но это не имело никакого значения.
Как только я прочитал ваш комментарий, я понял, что подпись не имеет смысла. Спасибо! Как отметил @drewtato, мне действительно хотелось, чтобы fn write_using_writer_a() принял Fn, для чего требуется Write; реализация fn write_using_writer_a() должна предоставлять возможность использования «писателя».
По вашим словам, вы «действительно хотите» использовать предоставленный вызывающим абонентом W: Write, но при этом обеспечивать замыкание с помощью &mut BufWriter<File>. Так что я не понимаю вашей цели.
Я переформулировал вопрос для ясности. Я знаю, что первоначальный вопрос вызвал путаницу относительно того, чего я пытался достичь. Надеюсь, теперь это ясно, и второй блок кода был моей попыткой решить проблему (неправильно, как заметили некоторые).

Единственное, что можно получить от замены BufWriter в подписи, — это ослабить гарантии вашего API, когда write_using_writer_a является общедоступным. Если вам это не нужно, оставьте BufWriter. Если да, то читайте дальше.
На самом деле вы не хотите, чтобы write_using_writer_a было слишком общим W. Это означало бы, что вызывающая сторона может решить, какой W использовать, но в вашем случае вам нужно принять функцию, которая принимает конкретную запись, но не хотите, чтобы вызывающая сторона знала, какую из них.
В Rust для этого уже есть синтаксис, который выглядит следующим образом:
fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
F: for<W: Write> Fn(W) -> Result<()>,
Это называется границей черты более высокого ранга (HRTB), но в настоящее время она работает только на протяжении всей жизни. Вместо этого вы можете создать свой собственный тип, который предоставляет только тот API, который вам нужен — признак Write — и заставить функцию использовать его.
pub struct WriteWrapper<'a>(&'a mut BufWriter<File>);
// implement all methods so that all behavior is preserved
impl Write for WriteWrapper<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
self.0.write_vectored(bufs)
}
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
self.0.write_all(buf)
}
fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
self.0.write_fmt(fmt)
}
}
fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
F: Fn(WriteWrapper) -> Result<()>,
{
let file = File::create(file)?;
let mut writer = BufWriter::new(file);
f(WriteWrapper(&mut writer))?;
writer.flush()?;
Ok(())
}
Теперь вы можете сделать write_using_writer_a общедоступным, и изменение внутреннего BufWriter не будет кардинальным изменением.
Интересный факт: эта функция использует неявный HRTB на протяжении всей жизни WriteWrapper. Это эквивалентно этому:
fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
F: for<'a> Fn(WriteWrapper<'a>) -> Result<()>,
Мне очень понравился ваш ответ по нескольким причинам. (1) подумайте, действительно ли мне нужно ослабить гарантии API: делая это явно BufWriter<File>, он намекает пользователю на сайте вызова, как Write реализован. На данный момент у меня есть два типа вызывающих программ: serde_json::to_writer, который принимает Write (в моем примере) и записывает байты в файл, например. writer.write_all(..), оба из которых, очевидно, пишут в файлы, что оправдывает BufWriter в подписи. Однако я (теоретически) думал, как я могу обобщить это.
и (2) само решение не является волшебным (см. решение @Jmb), но оно явное, поэтому легко увидеть, как оно работает. Раньше я сталкивался с границами признаков более высокого ранга в некоторых API, поэтому я знаю о времени жизни.
Если можно, у меня возникнет еще один вопрос, чтобы расширить свои знания. В вашей отключенной функции HRTB подпись гласит: «Я обещаю, что ссылка, хранящаяся в BufWriter, будет действительна до тех пор, пока вызывающий абонент использует ее в Fn (замыкание)», не так ли? И причина, по которой в этом случае это не обязательно, связана с правилами исключения Rust?
@madisonmuir 1) Да, и это самое слабое обещание, которое вы можете дать за всю жизнь. 2) Ага.
Вы пытаетесь сказать, что вызывающая сторона write_using_writer_a может предоставить замыкание, которое принимает некоторый тип W, который они выберут, но что бы они ни выбрали, вместо этого вы передадите &mut BufWriter<File>. Очевидно, это не может сработать.
Если вы не хотите называть BufWriter<File>, на самом деле вы хотите сказать, что вызывающая сторона write_using_writer_a должна предоставить замыкание, которое принимает некоторый тип W, который вы выбираете, и что им нужно знать только, что W реализует Write. В настоящее время это невозможно в стабильной версии Rust, но можно сделать каждую ночь, используя черту Impl в псевдонимах типов:
#![feature(type_alias_impl_trait)]
use std::fs::File;
use std::path::Path;
use std::io::{ BufWriter, Result, Write };
pub type MyWriter = impl Write;
pub fn write_using_writer_a<F>(file: &Path, f: F) -> Result<()>
where
F: Fn(&mut MyWriter) -> Result<()>,
{
// let _ = ensure_dir(file)?;
let file = File::create(file)?;
let mut writer = BufWriter::new(file);
let _ = f(&mut writer)?;
writer.flush()?;
Ok(())
}
Это очень круто; волшебство, я бы сказал. Безусловно, это самое простое решение, но мне нужно прочитать, как оно на самом деле работает (спасибо за ссылку и решение). Для моего приложения использование ночного компилятора не является проблемой, но для других это может оказаться неэффективным, и в этом случае я бы воспользовался решением, предоставленным @drewtato. Я бы хотел принять оба решения.
Как уже говорилось в другом ответе, это, вероятно, не то, что вы хотите. Но существуют сценарии, в которых необходим вызов функции с помощью универсальной функции.
Для этого нам нужно создать типаж, который будет выполнять функцию:
trait MyFun {
fn my_f<W: Write>(&self, w: W) -> Result<(), Error>;
}
Затем мы можем реализовать эту черту для «функций», которые мы отправляем в функцию:
struct ExampleFun;
impl MyFun for ExampleFun {
fn my_f<W: Write>(&self, w: W) -> Result<(), Error> {
todo!()
}
}
Фактическая функция, использующая это:
fn write_using_writer_a<F>(file: &Path, f: F) -> Result<(), Error>
where
F: MyFun
{
let file = File::create(file)?;
let mut writer = BufWriter::new(file);
let _ = f.my_f(&mut writer)?;
writer.flush()?;
Ok(())
}
И пример main:
fn main() {
write_using_writer_a(Path::new("/test.txt"), ExampleFun).unwrap();
}
То, что вы пытались написать, говорит: «Вызывающие
write_using_writer_aпредоставят функциюf, которая принимает тип параметраWпо их выбору, но независимо от этого выбора я затем вызовуfс&mut BufWriter<File>».